自定义零侵入的springboot-starter
背景:
突发奇想,有没有什么办法可以不需要在 springboot 的启动类上添加类似 @EnableEurekaClient、@EnableFeignClients、@EnableXXXXXXX 这样的注解,也不需要在代码里添加 @Configuration 类似的配置类,更不需要修改原有的代码, 仅需在 pom 中引入一个 jar 包,然后什么都不用做就能对项目的运行产生影响,或者随意支配。
想了下,要不就拿所有的 controller 方法执行前后打印一下 log 的功能来写一个 demo 实现一下吧。
打印 log ???这里为什么不用 aspect 写一个 aop 的切面来实现呢?因为这样你就要在 springboot 启动类上添加 @EnableAspectJAutoProxy 注解,在项目中申明切面,在每个 controller 中加上切面的注解,那这样不就产生了代码侵入了嘛。
![](https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/generic/v2-bc309f8a7eb0de9dd0c7277be07e9a46_1440w.jpg)
![](https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/generic/v2-bc309f8a7eb0de9dd0c7277be07e9a46_1440w.jpg)
![](https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/generic/v2-bc309f8a7eb0de9dd0c7277be07e9a46_1440w.jpg)
分析
既然要在所有的 controller 方法被调用的前后打印 log,那么我们就需要对这些 controller 进行增强,既然要增强,那么就需要用到代理,既然要使用代理,就需要知道在什么时候能对 controller 进行代理对象的包装,就需要对这些 controller 的创建过程了解,需要知道 spring 的 bean 在什么时候实例化完成,在什么时候扔进单例池,这其中哪个阶段,是音乐家(听说spring的作者是音乐家)留给开发者的勾子方法。
这里我们用到的是 BeanPostProcessor ,因为在 spring 中,所有的单例 bean 在实例化完成,丢进单例池之前的这个状态里,都会调用所有实现了 BeanPostProcessor 接口的 #postProcessAfterInitialization 方法对 bean 做相关的操作,我们利用 bean 生命周期中的这个时间点,对所有 bean 中凡是 controller 的 bean 进行增强,参考spring的aop、事务等实现原理生成代理对象
(###不过我不用启动类上加注解,以及搭配什么 @Import SelectImport Registry 等操作来实现。)
梳理了一下实现的方案,大致分为三个步骤:
- 第一步:我们需要在 controller 这个 bean 丢进单例池之前前添加拦截,需要用到 BeanPostProcessor 后置处理器来实现。
- 第二步:我们给所有拦截到的 controller 包装一层自定义的代理,方便在所有 controller 的方法在调用前后做一些自己的操作,此处用到的是 cglib 实现。
- 第三步:我们需要将我们拦截 controller 用到的 BeanPostProcessor 后置处理器被 spring 框架加载并调用,这里用到了 SPI 设计模式,使用 spring.factories 协助来实现。
-
第一步:
为什么要在 controller 这个 bean 丢进单例池之前前添加拦截,是因为 springMVC 开始维护 controller 的 handler、method、url 关系映射的时候,都是建立在所有的 bean 已经实例化完成之后,在单例池中获取 bean 的信息,参考[AbstractHandlerMethodMapping->#afterPropertiesSet],所以,我们需要在 bean 实例化完成之前,就对 bean 进行代理对象的生成,将生成好的代理对象丢进单例池中,而不影响其他业务逻辑,所以我们借助 bean 生命周期中的最会一环-BeanPostProcessor#postProcessAfterInitialization 来实现。 -
第二步:
这里偷个懒,直接用 cglib 生成了 controller bean 的代理对象,因为 jdk 代理生成后的动态对象在 springMVC 维护 controller、method、url 映射关系的时候,无法识别当前 jdk 生成的 jdk 动态代理对象是否是 controller 对象,因为框架没有获取到代理对象的真实对象类型,不过感觉理论上是有办法解决的。 -
第三步:
借助 spring 启动流程中较为早期的环节,加载 ApplicationContextInitializer 实现类的环节,我们把我们的对象交给 spring 容器去管理,此时我们通过 spring.factories 来配置我们的实现类,以此达到了代码无侵入的目的。
具体实现:
打算弄两个项目,一个是 starter 项目,一个是 springboot 项目,然后 springboot 项目中引用 starter 项目,写在一个项目里面也行。
首先,我们先新建一个空的 maven 项目,作为 starter 功能编写的项目,项目的 group、artifactId 等信息如下:
groupId = com.summer
artifactId = my-spring-starter
version = 1.0-SNAPSHOT
在该项目的 pom 中添加相关依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
依赖中使用到了 spring-context ,我们要用的相关扩展点基本上全都在 spring-context 中,但是我们还引入了 spring-web 依赖,因为 @RestController、@Mapping 和 @RequestMapping 三个注解都在 spring-web 依赖中,而我们想要确定一个 bean 是否是 controller,我们需要用到四个注解分别是 @Controller、@RestController、@Mapping 和 @RequestMapping, spring-context 只有 @Controller 注解,满足不了需求。
然后在主目录下的src/main/java路径下,新建一个 java POJO,叫做 MySpringStarterApplicationContextInitializer,全路径为
com.summer.starter.initializer.MySpringStarterApplicationContextInitializer
在该类中,我们实现了 ApplicationContextInitializer ,重写 initialize 方法,在方法中注册了一个 BeanDefinitionRegistryPostProcessor 的实现类 MyBeanDefinitionRegistryPostProcessor。之所以实现 ApplicationContextInitializer 一是为了无侵入做铺垫,我们通过springboot启动全周期的spring.factories配置我们的MySpringStarterApplicationContextInitializer类,就能在springboot启动流程中,较为前期的准备上下文的阶段加载我们的类文件到系统中,以此达到无侵入的目的,二是因为通过该类,可以将我们后期想要做相关逻辑处理的一些对象注册到spring容器中,去实现更多的想要做的事情。
然后再新建一个 MyBeanDefinitionRegistryPostProcessor 实现类,或者就写在当前类中都可以。
在 MyBeanDefinitionRegistryPostProcessor 类中,我们实现了 BeanDefinitionRegistryPostProcessor 和 Ordered,重写 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法,注册一个 ControllerEnhanceBeanPostProcessor 对象,该对象中包含了最核心的逻辑,同时,实现了 Ordered 接口,设置了该 BeanFactoryPostProcessor 实现类的执行顺序为最晚执行。
其中 ControllerEnhanceBeanPostProcessor 是一个 BeanPostProcessor 接口的实现类, BeanPostProcessor 接口的两个方法分别作用于 bean 的 IOC 阶段完成,实例化操作开始之前的阶段,以及实例化已经完成,放进单例池之前的阶段。我们实现 BeanPostProcessor 接口,目的是为了利用实例化已经完成,放进单例池之前的这个阶段,在这个期间,spring框架会将对 bean 传到这个方法中,此时可以做随意的修改,并将修改后的 bean 还给 spring 框架,我们对 controller 对象做一层代理的封装,就在这个实例化完成,放进单例池之前的这个阶段,以此达到前期的设想。
ControllerEnhanceBeanPostProcessor 的全部代码如下:
package com.summer.starter.processor;
import com.summer.starter.proxy.ControllerEnhanceInterceptor;
import com.summer.starter.proxy.ControllerEnhanceInvocationHandler;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.lang.annotation.Annotation;
import java.lang.reflect.Proxy;
import java.util.concurrent.ConcurrentHashMap;
/**
* 控制器增强后置处理
*/
public class ControllerEnhanceBeanPostProcessor implements BeanPostProcessor, EnvironmentAware {
/**
* 增强log是否打开
*/
public static enum EnhanceLogEnum {
LOG_ON,
LOG_OFF;
private EnhanceLogEnum() {
}
}
/**
* 记录已经创建过代理对象的 bean
*/
private ConcurrentHashMap<String, Object> beanCache = new ConcurrentHashMap<>();
//增强 log 配置 key
private static final String enhanceLogOpenEnv = "spring.controller.enhance.log.open";
//是否开启增强log
private boolean enhanceLogOpen = true;
//可以拿到 application.yml 的配置信息
@Override
public void setEnvironment(Environment environment) {
//读取配置中的设置
String openLogSetting = environment.getProperty(enhanceLogOpenEnv);
if (EnhanceLogEnum.LOG_OFF.name().toLowerCase().equals(openLogSetting)) {
enhanceLogOpen = false;
}
}
/**
* 实例化完成,放进单例池之前的阶段
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//是否是 controller 对象
boolean hasControllerAnnotation = false;
Class<?>[] interfaces = bean.getClass().getInterfaces();
if (interfaces.length <= 0) {
//检验是否是 controller bean 普通对象 bean.getClass() 就可以获取到 class 的 Annotation 信息
hasControllerAnnotation = matchController(bean.getClass());
} else {
//被springboot处理过的代理对象需要获取 super class 才能拿到真实的 class 的 Annotation 信息,否则拿不到注解信息
//检验是否是 controller bean
hasControllerAnnotation = matchController(bean.getClass().getSuperclass());
}
//如果是 controller bean 创建代理对象 //如果是 controller bean 创建代理对象
if (hasControllerAnnotation) {
return this.creatCglibProxy(bean, beanName, enhanceLogOpen);
}
//返回默认 bean
return bean;
}
/**
* 递归获取包含 base 中是否带有四个标签的注解来判断是否是 controller
*
* @param clazz
* @return
*/
private boolean matchController(Class<?> clazz) {
for (Annotation annotation : clazz.getAnnotations()) {
if (annotation instanceof Controller
|| annotation instanceof RestController
|| annotation instanceof Mapping
|| annotation instanceof RequestMapping) {
return true;
}
}
if (clazz.getSuperclass() != null) {
matchController(clazz.getSuperclass());
}
return false;
}
/**
* 创建代理对象
*
* @param bean
* @param beanName
* @param enhanceLogOpen
* @return
*/
private Object creatJdkProxy(Object bean, String beanName, boolean enhanceLogOpen) {
Object beanCache = this.beanCache.get(beanName);
if (beanCache != null) {
return beanCache;
}
//ControllerEnhanceInvocationHandler jdk代理对象
ControllerEnhanceInvocationHandler invocationHandler = new ControllerEnhanceInvocationHandler(bean, enhanceLogOpen);
Object proxyBean = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), invocationHandler);
this.beanCache.put(beanName, proxyBean);
return proxyBean;
}
/**
* 创建代理对象
*
* @param bean
* @param beanName
* @param enhanceLogOpen
* @return
*/
private Object creatCglibProxy(Object bean, String beanName, boolean enhanceLogOpen) {
Object beanCache = this.beanCache.get(beanName);
if (beanCache != null) {
return beanCache;
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(bean);
proxyFactory.addAdvice(new ControllerEnhanceInterceptor(enhanceLogOpen));
Object proxyBean = proxyFactory.getProxy();
this.beanCache.put(beanName, proxyBean);
return proxyBean;
}
}
ControllerEnhanceBeanPostProcessor 对象实现了 BeanPostProcessor接口 与 EnvironmentAware 接口,我们需要的实例化完成,放进单例池之前的阶段是在 BeanPostProcessor 接口的 postProcessAfterInitialization 方法中,对于 controller 做一层代理封装的操作,也是从这个方法开始。而 EnvironmentAware 接口则是为我们提供项目的配置文件信息,在 setEnvironment 方法中,配置文件可有可无,此处做功能测试,以获取配置控制 log 开关为实验。
![](https://summer-blog-images.oss-cn-shanghai.aliyuncs.com/spring_boot_starter/log_enums.png)
该类文件最上方是 EnhanceLogEnum 枚举对象,其实可有可无,就是拿来配置所有 controller 中的方法执行前后是否开启 log 打印的功能而已,直接在 application.yml 中使用 1、2数值或者 true/false 的布尔值都能实现。
[上图序号1处] beanCache 是为了解决对象重复创建的问题,理论上是不存在的,因为每个 bean 只会经过该方法一次的调用。
[上图序号2处] enhanceLogOpenEnv 是 application.yml 文件中的配置 key。
[上图序号3处] enhanceLogOpen 代表是否开启所有 controller 中的方法执行前后的 log 打印的功能,默认开启,如果 application.yml 配置了 enhanceLogOpenEnv,以配置为主。
[上图序号4处] setEnvironment 方法会将目前最新的项目配置文件信息暴露出来,此时也可以往里面添加一些新的配置,但是目前只是为了使用它获取我们需要的 enhanceLogOpenEnv 配置来判断是否需要关闭所有 controller 中的方法执行前后 log 打印的功能。
postProcessAfterInitialization 方法中的逻辑是判断当前的 bean 是否是 controller 对象,是的话,则为 controller 对象创建 cglib 的代理对象,jdk代理对象的方式,这里省略了,否则什么也不操作,直接返回当前的对象。
判断是否为 controller 调用的是 matchController 方法,通过四个注解( Controller、RestController、Mapping 、RequestMapping)判断一个 bean 是否为 controller,如果没找到的话,递归查找父类是否为 controller。
如果是 controller 则调用 creatCglibProxy 方法,创建 cglib 的代理对象,对象用到了 ControllerEnhanceInterceptor 对象,在 ControllerEnhanceInterceptor 中实现了对当前 controller 中的所有方法做增强的逻辑。
ControllerEnhanceInterceptor 对象实现了 MethodInterceptor,其实就是实现了 Advice 接口,主要的目的就是做增强,在 invoke 方法中,对 controller 方法 (Object proceed = invocation.proceed()) 调用的前后做增强。
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
com.summer.starter.initializer.MySpringStarterApplicationContextInitializer
到这里代码部分已经都完成了,接下来,要配置 spring.factories ,我们在项目的 resource 文件夹下新建一个 META-INF 文件夹,在 META-INF 文件夹中新建一个 spring.factories 的文件,在文件中填入我们的 ApplicationContextInitializer 实现类的全包路径。
!!! 至此,starter 就已经写好了,install 一下,将依赖打包到本地maven仓库中。
此时新建一个 springboot 项目,项目中引入刚刚的 starter 测试一下效果。
通过测试,发现结果和预期的效果一致,springboot 中仅仅引入了 jar 包,就能实现相关的控制,零业务代码侵入,有了 spring-context 中的这些扩展点,对整个框架的功能可以做很多很多的扩展。
github地址 https://github.com/GITHUBFORSUMMER/spring-starter
我的个人网站 https://www.huangyingsheng.com/2021/07/10/6b23595c-5c45-e08c-d80a-7fc2582df0ba/