Spring编程使用DDD的小把戏

场景

现在流行充血领域层,在原本只存储对象的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 {@Autowiredprivate Service Service;/*** 保存自身*/@Transactionalpublic 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 {@Autowiredprivate Service Service;/*** 保存自身*/@Transactionalpublic 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 {...@Overridepublic 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.hqwc.cn/news/690668.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【免费Java系列】大家好 ,今天是学习面向对象高级的第十二天点赞收藏关注,持续更新作品 !

这是java进阶课面向对象第一天的课程可以坐传送去学习http://t.csdnimg.cn/Lq3io day10-多线程 一、多线程常用方法 下面我们演示一下getName()、setName(String name)、currentThread()、sleep(long time)这些方法的使用效果。 public class MyThread extends Thread{publi…

力扣例题(用栈实现队列)

目录 链接. - 力扣&#xff08;LeetCode&#xff09; 描述 思路 push pop peek empty 代码 链接. - 力扣&#xff08;LeetCode&#xff09; 描述 思路 push 例如我们将10个元素放入栈中&#xff0c;假设最左边为栈顶&#xff0c;最右侧为栈底 则为10,9,8,7,6,5,4,3,…

stm32——OLED篇

技术笔记&#xff01; 一、OLED显示屏介绍&#xff08;了解&#xff09; 1. OLED显示屏简介 二、OLED驱动原理&#xff08;熟悉&#xff09; 1. 驱动OLED驱动芯片的步骤 2. SSD1306工作时序 三、OLED驱动芯片简介&#xff08;掌握&#xff09; 1. 常用SSD1306指令 2. …

清理缓存简单功能实现

在程序开发中&#xff0c;经常会用到缓存&#xff0c;最常用的后端缓存技术有Redis、MongoDB、Memcache等。 而有时候我们希望能够手动清理缓存&#xff0c;点一下按钮就把当前Redis的缓存和前端缓存都清空。 功能非常简单&#xff0c;创建一个控制器类CacheController&#xf…

Docker搭建ctfd平台

安装docker和docker-compose &#xff08;1&#xff09;安装docker&#xff1a; curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun&#xff08;2&#xff09;安装 Docker Compose&#xff1a; yum install docker-compose安装失败参考下面文章 https:/…

小程序获取手机号,用户昵称,头像

一、手机号 在微信小程序中&#xff0c;获取用户手机号也需要用户的明确授权。你可以使用 button 组件的 open-type 属性设置为 getPhoneNumber 来实现这个功能。当用户点击这个按钮时&#xff0c;会弹出一个对话框请求用户的授权。如果用户同意&#xff0c;你可以在 bindgetp…

C++入门——命名空间、缺省参数、函数重载、引用、内敛函数、auto关键字

目录 前言 一、什么是C 1.1 C关键字(C98) 二、命名空间 2.1 命名空间定义 1.正常命名空间的定义 2.命名空间的定义可以嵌套 3.同名的命名空间会合并 2.2 命名空间的使用 三、C输入&输出 四、缺省参数 4.1 缺省参数概念 4.2 缺省参数分类 五、函数重载 5.1 …

draw.io 网页版二次开发(3):打包和部署(war包)

目录 一 说明 二 环境配置 1. 下载并安装 Apache Ant 2. 下载并安装JDK和JRE 3. 下载tomcat 4. Ant、JDK和JRE 环境变量的配置 三 draw.io打包 四 部署 五 最后 一 说明 应公司项目要求&#xff0c;需要对draw.io进行二次开发&#xff0c;并将html界面通过iframe 嵌…

Github上 5 个好玩儿的开源项目

1. 在你的 Windows 养小猫 2. 把你的图片生成 ASCII 3. 中国制霸生成器 4. 像素风格代码字体 5. 梦回 QQ 空间 01 在你的 Windows 养小猫 在MacBook的触摸板上&#xff0c;你可以抚养一只小宠物&#xff0c;并与它互动、喂食&#xff0c;这样非常有趣。 我向你推荐了一个…

六级翻译笔记

理解加表达 除了专有名词不能自己理解翻译&#xff0c;其它都可以 时态一般唯一 题目里出现有翻译为 客观存在&#xff1a; there be 单词结尾加er和ee的区别&#xff1a;er是主动&#xff0c;ee是被动 中文句子没有被动&#xff0c;也可以英文翻译为被动 中文的状语可以不是…

Python | Leetcode Python题解之第84题柱状图中最大的矩形

题目&#xff1a; 题解&#xff1a; class Solution:def largestRectangleArea(self, heights: List[int]) -> int:n len(heights)left, right [0] * n, [n] * nmono_stack list()for i in range(n):while mono_stack and heights[mono_stack[-1]] > heights[i]:righ…

如何根据招聘信息打造完美简历

如何根据招聘信息打造完美简历 招聘信息分析简历调整策略个性化与关键词结语 在求职过程中&#xff0c;简历是第一块敲门砖。它不仅展示了你的专业技能和工作经验&#xff0c;还体现了你对所申请职位的理解和热情。然而&#xff0c;如何从招聘信息中提炼关键点&#xff0c;打造…