Spring编程使用DDD的小把戏

news2024/9/23 0:30:13

场景

现在流行充血领域层,在原本只存储对象的java类中,增加一些方法去替代原本写在service层的crud,

但是例如service这种一般都是托管给spring的,我们使用的ORM也都托管给spring,这样方便在service层调用mybatis的mapper、或者jpa这种和spring结合的类,使用自动注入 @Resource、@Autowired 就行了,

但是像entity、vo这种保存信息的对象,一般都是直接new的,或者从数据库中查出然后被映射出来的,以及从前端传入到接口层的,它们并没有被spring托管,

如果在他们自身中想去使用注解引入spring相关的类,则无法实现,通过SpringContext.getBean这种硬编码感觉又不大雅观。

需求

我的应用场景是从Rest接口传入的参数,例如 save接口、update接口,通过 @RequestBody 传入的对象自身能不能直接调用Spring中的Service来实现自身的CRUD?这样自身可以完成一些验证以及数据库交互操作,并且代码也内聚在自身逻辑里,不会让service中充斥过多的CRUD代码,造成阅读代码上的不方便,每个参数中有自己的逻辑,并且不会很多,读起来就会相对清晰些。

举例

例如下面的代码,我想让参数自身就可以进行对数据库的交互,而不是将params传给service,然后在service中进行处理,

需要考虑的问题就是,如何让 SaveParams 这种前端接收的参数能被spring托管,这样就可以使用spring的bean了。

/**
 * 保存
 * @param params
 * @return
 */
@PostMapping("save")
public RestResponse save(@RequestBody @Validated SaveParams params) {
    params.save();
    return success();
}

/**
 * 更新
 * @param params
 * @return
 */
@PostMapping("update")
public RestResponse update(@RequestBody @Validated UpdateParams params) {
    params.update();
    return success();
}

参数内部

参数内部是这个样子,但是这样肯定是不行的,用起来一定会报错,因为 SaveParams 并不属于spring的bean,

而是spring mvc的参数解析,将前端传入的参数构建成了 SaveParams

public class SaveParams extends Vo {


    @Autowired
    private Service Service;

    /**
     * 保存自身
     */
    @Transactional
    public void save() {
        Service.save(this);
    }

}

OK,知道问题所在,那么想让他成为spring的bean,第一步我们应该是给他头上也加上spring的注解,对吧?修改如下:

@Component 使用这个注解将类注册成spring bean的注解,为什么还要加上 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) ?

因为spring bean默认是单例模式,加上上面的Scope意味着每次都实例化创建一个新的bean,这符合我们的需求,

因为我们的接口每次收到请求都是一个全新的参数,自然不可能用单例的,每个接口都是自己的一个生命周期。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class SaveParams extends Vo {


    @Autowired
    private Service Service;

    /**
     * 保存自身
     */
    @Transactional
    public void save() {
        Service.save(this);
    }

}

问题2

结合上面的接口代码和对params增加的注解就可以直接使用了吗?显然不是,因为接口接收参数时,并不会因为我们的参数类加上了注解就帮我们注册成一个spring的bean给到我们,

我们需要自己去做这个事情,就是在接口接收到这个参数的时候,我们需要将他变成spring的bean,我们需要做一个拦截,做一个参数的篡改。

实现将接收参数变成spring bean

如何篡改?选时机即可,就选在接口刚接收到这个参数并解析完毕的时候,

我们利用spring给我们的拦截点 RequestBodyAdvice ,在接口接收完毕参数后,检验参数是否存在 @Component 注解,

如果存在,则使用 SpringUtils.getBean 从spring容器中新创建一个bean出来,

然后将之前的参数复制到这个bean里面来,这样这个bean既拿到了参数,又拿到了spring容器中自动注入的其他bean,二者结合,这个params就可以自己玩了,

这里实体之间的复制我使用了 BeanUtils.copyProperties(o, newObject, ignoreFields.toArray(new String[]{})); ,因为老参数里面的自动注入bean一定会是null,直接用会把spring新创建的给覆盖掉,所以这里要忽略一下自动注入的属性,

这时候我们把新的bean,返回回去,在接口里,params自己调用自己的save方法就不会报错了。

@Slf4j
@ControllerAdvice
public class DDDParamAdvice implements RequestBodyAdvice {


	...

	@Override
	public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
		Object newObject = null;
		try {
			// 判断该对象类上是否存在 Component 注解
			if (o.getClass().isAnnotationPresent(Component.class)) {
				newObject = SpringUtils.getBean(o.getClass());
				Field[] fields = ReflectUtil.getFields(o.getClass());
				List<String> ignoreFields = new ArrayList<>();
				if (ArrayUtil.isNotEmpty(fields)) {
					for (Field field : fields) {
						if (field.isAnnotationPresent(Autowired.class)) {
							ignoreFields.add(field.getName());
						}
					}
				}
				BeanUtils.copyProperties(o, newObject, ignoreFields.toArray(new String[]{}));
			}
		} catch (Exception e) {
			log.error("DDDParamAdvice error", e);
		}
		return newObject != null ? newObject : o;
	}

    ...

}

还是有问题

到这一步虽然参数被转换成了spring中的bean,可以自己玩转了,但是并没有结束,

我在接口中使用 @Validated 验证时,发现验证不会通过,但是参数实际上是有值的,

通过排查我发现是因为spring的bean,cglib生成子类后,将属性拷贝一份到子类来,子类中的并没有值,

但是使用get方法还是可以正常获取到,具体情况如下图,看似没值,但是get其实有值,

在这里插入图片描述

在这里插入图片描述
真正的值其实被存储在 CGLIB$CALLBACK_1 中,并且可以看到Service其实也已经被注入:

在这里插入图片描述

如何解决

因为 @Validated 验证时机在 RequestBodyAdvice 之后,那么有没有一种办法在通过验证后,我们再将参数转成spring的bean呢?

答案当然是有,参考这篇blog:@Valid @Validated与先于AOP的执行顺序问题

使用AOP拦截controller方法,会让验证在前,AOP在后,所以我们使用AOP来替换参数为bean,而不使用 RequestBodyAdvice 就好了。

所以我们更改拦截如下:

@Component
@Aspect
public class DDDParamsAop {

    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void aspect() {
    }


    @Around("aspect()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        if (joinPoint.getArgs().length == 1) {
            Object o = joinPoint.getArgs()[0];
            if (o.getClass().isAnnotationPresent(Component.class)) {
                Object newObject = SpringUtils.getBean(o.getClass());
                Field[] fields = ReflectUtil.getFields(o.getClass());
                List<String> ignoreFields = new ArrayList<>();
                if (ArrayUtil.isNotEmpty(fields)) {
                    for (Field field : fields) {
                        if (field.isAnnotationPresent(Autowired.class)) {
                            ignoreFields.add(field.getName());
                        }
                    }
                }
                BeanUtils.copyProperties(o, newObject, ignoreFields.toArray(new String[]{}));
                joinPoint.getArgs()[0] = newObject;
                return joinPoint.proceed(joinPoint.getArgs());
            }
        }
        return joinPoint.proceed();
    }
}

结束问题

通过更改篡改参数时机,我们绕过了验证器的问题,并且让我们的参数可以自身注入spring其他bean完成相应的逻辑。

结语

上面编写的代码并不完善,例如对参数的拦截点,只拦截了PostMapping, 忽略的参数只忽略了 @Autowired 注解,基本只覆盖了我自身使用的场景,

但是基于这个原理,可以自行拓展,对更多的场景进行适配,完成对Service 代码和逻辑的拆解,将独立的功能封装到各自的实体领域中,方便代码管理与阅读,并且职责清晰。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1679681.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PostgreSQL扩展之PGroonga:多语言全文搜索

简介 PGroonga 是一个 PostgreSQL 扩展&#xff0c;它增加了基于 Groonga 的全文搜索索引方法。虽然原生的 PostgreSQL 支持全文索引&#xff0c;但它仅限于基于字母和数字的语言。PGroonga 提供了更广泛的字符支持&#xff0c;使其成为 PostgreSQL 支持的语言的超集&#xff…

malloc_consolidate

此文章用于详细介绍malloc_consolidate。 众所周知&#xff0c;fastbin一般是不能合并&#xff0c;但在malloc_consolidate中是个例外。 1.触发机制 首先构造这样的堆块结构 一个0x40的堆块在fastbin中&#xff0c;一个0x110的堆块在unbin中 随后我们尝试分配一个0x300的堆…

NSSCTF | [SWPUCTF 2021 新生赛]easyupload2.0

先传一个普通的一句话木马试一试 GIF89a <?php eval($_POST[shell]);?> 可以看到回显&#xff0c;不允许上传php文件。 使用Burpsuite抓包只修改ContentType后发现也不能绕过&#xff0c;说明服务器使用了黑名单后缀限制&#xff0c;那么我们可以使用其他的后缀代替ph…

ubuntu CUDA 驱动更新,版本更新,多CUDA版本管理

1 新版本驱动下载 前面介绍过window CUDA驱动更新&#xff0c;但是对于ubuntu 的驱动更新&#xff0c;没有一键操作。 本人笔记本电脑n年前装的CUDA DRIVER仅支持到cuda10.2&#xff0c;实在无法满足这日新月异的科技更新。 左 旧的驱动版本 右 新下载的硬件支持的驱动版本&…

使用RN的kitten框架的日历组件的修改

官方网页地址 下面就是我参考官方封装的时间日期组件&#xff08;主要是功能和使用方法&#xff0c;页面粗略做了下&#xff0c;不好看勿怪&#xff09; import React, {useState} from react; import {StyleSheet, View, TouchableOpacity, SafeAreaView} from react-native; …

运用MongoDB Atlas释放开发者潜能同时把控成本

在当下的商业环境中&#xff0c;不可预测性已经成为常态&#xff0c;工程团队负责人必须在把控不可预测性和优化IT成本的双重挑战下谋求平衡。 咨询公司德勤2024 MarginPLUS调查收集了300多位企业负责人的见解&#xff0c;报告中重点介绍了面对动荡的全球经济环境&#xff0c;…

kubernetes多master集群架构

一、完成master02节点的初始化操作 master02环境准备&#xff0c;详细过程参考上一期博客环境准备 #添加主机映射 vim /etc/hosts 192.168.88.3 master01 192.168.88.8 master02 192.168.88.4 node01 192.168.88.5 node021、准备master02节点需要的文件 从 master01 节点上拷…

数学:矩阵范数的定义、常见的矩阵范数

1 算子范数【从属范数】 1.1 1-算子范数【列和范数】 &#xff1a;即对A的每列的绝对值求和再求其中的最大值 1.2 ∞-算子范数【行和范数】即对 A 的每行的绝对值求和再求其中的最大值 1.3 2-算子范数【谱范数】 学过奇异值分解就知道谱范数是最大奇异值/ 二次型的最大特…

大数据Spark教程从入门到精通第四篇:Spark快速上手

一&#xff1a;Spark快速上手 1&#xff1a;创建Maven项目 idea安装scala_idea scala插件-CSDN博客 代表了我们安装scala的maven环境已经准备好了&#xff0c;代码可以正常跑了

kafka用java收发消息

用java客户端代码来对kafka收发消息 具体代码如下 package com.cool.interesting.kafka;import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; i…

2024042001-计算机网络 - 物理层

计算机网络 - 物理层 计算机网络 - 物理层 通信方式带通调制 通信方式 根据信息在传输线上的传送方向&#xff0c;分为以下三种通信方式&#xff1a; 单工通信&#xff1a;单向传输半双工通信&#xff1a;双向交替传输全双工通信&#xff1a;双向同时传输 带通调制 模拟信号…

程序验证之Dafny--证明霍尔逻辑的半自动化利器

一、What is Dafny?【来自官网介绍 Dafny 】 1)介绍 Dafny 是一种支持验证的编程语言&#xff0c;配备了一个静态程序验证器。 通过将复杂的自动推理与熟悉的编程习语和工具相结合&#xff0c;使开发者能够编写可证明正确的代码&#xff08;相对于 {P}&#xff33;{Q} 这种…

数据结构(C):树的概念和二叉树初见

目录 &#x1f37a;0.前言 1.树概念及结构 2.认识一棵树 3.树的表示 3.1树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff09; 4.二叉树 4.1特殊的二叉树 4.2二叉树的性质 &#x1f48e;5.结束语 &#x1f37a;0.前言 言C之言&#xff0c;聊C之识&…

OpenAI 震撼发布:GPT-4o免费,实时语音视频交互开启新纪元

OpenAI 震撼发布&#xff1a;GPT-4o免费&#xff0c;实时语音视频交互开启新纪元 在仅仅问世17个月后&#xff0c;OpenAI 研制出了仿佛科幻片中登场的超级人工智能——GPT-4o&#xff0c;而且所有人都可以完全免费使用&#xff0c;让这个科技界的巨浪让人震撼无比&#xff01;…

JSP技术

前言&#xff1a;虽然现在Vue盛行&#xff0c;但是对于初学者和一些项目我们还是采用jsp技术来编写前端代码&#xff0c;一些老的项目也需要jsp去维护。就像老师说的法国的银行系统还是采用COBOL这种古老语言。本篇文章主要介绍jsp技术。 目录 一、概述 &#xff08;1&#…

Rust构造JSON和解析JSON

目录 一、Rust构造JSON和解析JSON 二、知识点 serde_json JSON 一、Rust构造JSON和解析JSON 添加依赖项 cargo add serde-json 代码&#xff1a; use serde_json::{Result, Value};fn main() -> Result<()>{//构造json结构 cpu_loadlet data r#"{"…

docker安装minio附带图片

1.拉镜像 docker pull minio/minio 2.创建挂载点目录 mkdir -p /usr/local/minio/config mkdir -p /usr/local/minio/data 3.创建minio容器 docker run \ -p 19000:9000 \ -p 9090:9090 \ --nethost \ --name minio \ -d --restartalways \ -e "MINIO_ACCESS_KEYmini…

【前端】CSS基础(4)

文章目录 前言1、CSS常用属性1.1 文本属性1.1.1 文本对齐1.1.2 文本装饰1.1.3 文本缩进1.1.5 行高 前言 这篇博客仅仅是对CSS的基本结构进行了一些说明&#xff0c;关于CSS的更多讲解以及HTML、Javascript部分的讲解可以关注一下下面的专栏&#xff0c;会持续更新的。 链接&…

第 397 场 LeetCode 周赛题解

A 两个字符串的排列差 模拟&#xff1a;遍历 s s s 记录各字符出现的位置&#xff0c;然后遍历 t t t 计算排列差 class Solution {public:int findPermutationDifference(string s, string t) {int n s.size();vector<int> loc(26);for (int i 0; i < n; i)loc[s…

一种基于电场连续性的高压MOSFET紧凑模型,用于精确表征电容特性

来源&#xff1a;A Compact Model of High-Voltage MOSFET Based on Electric Field Continuity for Accurate Characterization of Capacitance&#xff08;TED 24年&#xff09; 摘要 本文提出了一种新的高压MOSFET&#xff08;HV MOS&#xff09;紧凑模型&#xff0c;以消…