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 {


    @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 代码和逻辑的拆解,将独立的功能封装到各自的实体领域中,方便代码管理与阅读,并且职责清晰。

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值