自定义零侵入的springboot-starter

背景:

突发奇想,有没有什么办法可以不需要在 springboot 的启动类上添加类似 @EnableEurekaClient、@EnableFeignClients、@EnableXXXXXXX 这样的注解,也不需要在代码里添加 @Configuration 类似的配置类,更不需要修改原有的代码, 仅需在 pom 中引入一个 jar 包,然后什么都不用做就能对项目的运行产生影响,或者随意支配。

想了下,要不就拿所有的 controller 方法执行前后打印一下 log 的功能来写一个 demo 实现一下吧。

打印 log ???这里为什么不用 aspect 写一个 aop 的切面来实现呢?因为这样你就要在 springboot 启动类上添加 @EnableAspectJAutoProxy 注解,在项目中申明切面,在每个 controller 中加上切面的注解,那这样不就产生了代码侵入了嘛。

自定义零侵入的springboot-starter自定义零侵入的springboot-starter自定义零侵入的springboot-starter

分析

既然要在所有的 controller 方法被调用的前后打印 log,那么我们就需要对这些 controller 进行增强,既然要增强,那么就需要用到代理,既然要使用代理,就需要知道在什么时候能对 controller 进行代理对象的包装,就需要对这些 controller 的创建过程了解,需要知道 spring 的 bean 在什么时候实例化完成,在什么时候扔进单例池,这其中哪个阶段,是音乐家(听说spring的作者是音乐家)留给开发者的勾子方法。

这里我们用到的是 BeanPostProcessor ,因为在 spring 中,所有的单例 bean 在实例化完成,丢进单例池之前的这个状态里,都会调用所有实现了 BeanPostProcessor 接口的 #postProcessAfterInitialization 方法对 bean 做相关的操作,我们利用 bean 生命周期中的这个时间点,对所有 bean 中凡是 controller 的 bean 进行增强,参考spring的aop、事务等实现原理生成代理对象

(###不过我不用启动类上加注解,以及搭配什么 @Import SelectImport Registry 等操作来实现。)


自定义零侵入的springboot-starter


梳理了一下实现的方案,大致分为三个步骤:

  • 第一步:我们需要在 controller 这个 bean 丢进单例池之前前添加拦截,需要用到 BeanPostProcessor 后置处理器来实现。
  • 第二步:我们给所有拦截到的 controller 包装一层自定义的代理,方便在所有 controller 的方法在调用前后做一些自己的操作,此处用到的是 cglib 实现。
  • 第三步:我们需要将我们拦截 controller 用到的 BeanPostProcessor 后置处理器被 spring 框架加载并调用,这里用到了 SPI 设计模式,使用 spring.factories 协助来实现。
  1. 第一步:
    为什么要在 controller 这个 bean 丢进单例池之前前添加拦截,是因为 springMVC 开始维护 controller 的 handler、method、url 关系映射的时候,都是建立在所有的 bean 已经实例化完成之后,在单例池中获取 bean 的信息,参考[AbstractHandlerMethodMapping->#afterPropertiesSet],所以,我们需要在 bean 实例化完成之前,就对 bean 进行代理对象的生成,将生成好的代理对象丢进单例池中,而不影响其他业务逻辑,所以我们借助 bean 生命周期中的最会一环-BeanPostProcessor#postProcessAfterInitialization 来实现。

  2. 第二步:
    这里偷个懒,直接用 cglib 生成了 controller bean 的代理对象,因为 jdk 代理生成后的动态对象在 springMVC 维护 controller、method、url 映射关系的时候,无法识别当前 jdk 生成的 jdk 动态代理对象是否是 controller 对象,因为框架没有获取到代理对象的真实对象类型,不过感觉理论上是有办法解决的。

  3. 第三步:
    借助 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

自定义零侵入的springboot-starter

在该类中,我们实现了 ApplicationContextInitializer ,重写 initialize 方法,在方法中注册了一个 BeanDefinitionRegistryPostProcessor 的实现类 MyBeanDefinitionRegistryPostProcessor。之所以实现 ApplicationContextInitializer 一是为了无侵入做铺垫,我们通过springboot启动全周期的spring.factories配置我们的MySpringStarterApplicationContextInitializer类,就能在springboot启动流程中,较为前期的准备上下文的阶段加载我们的类文件到系统中,以此达到无侵入的目的,二是因为通过该类,可以将我们后期想要做相关逻辑处理的一些对象注册到spring容器中,去实现更多的想要做的事情。

然后再新建一个 MyBeanDefinitionRegistryPostProcessor 实现类,或者就写在当前类中都可以。

自定义零侵入的springboot-starter

在 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 开关为实验。


自定义零侵入的springboot-starter

该类文件最上方是 EnhanceLogEnum 枚举对象,其实可有可无,就是拿来配置所有 controller 中的方法执行前后是否开启 log 打印的功能而已,直接在 application.yml 中使用 1、2数值或者 true/false 的布尔值都能实现。


自定义零侵入的springboot-starter

[上图序号1处] beanCache 是为了解决对象重复创建的问题,理论上是不存在的,因为每个 bean 只会经过该方法一次的调用。

[上图序号2处] enhanceLogOpenEnv 是 application.yml 文件中的配置 key。

[上图序号3处] enhanceLogOpen 代表是否开启所有 controller 中的方法执行前后的 log 打印的功能,默认开启,如果 application.yml 配置了 enhanceLogOpenEnv,以配置为主。

[上图序号4处] setEnvironment 方法会将目前最新的项目配置文件信息暴露出来,此时也可以往里面添加一些新的配置,但是目前只是为了使用它获取我们需要的 enhanceLogOpenEnv 配置来判断是否需要关闭所有 controller 中的方法执行前后 log 打印的功能。


自定义零侵入的springboot-starter


postProcessAfterInitialization 方法中的逻辑是判断当前的 bean 是否是 controller 对象,是的话,则为 controller 对象创建 cglib 的代理对象,jdk代理对象的方式,这里省略了,否则什么也不操作,直接返回当前的对象。

自定义零侵入的springboot-starter

判断是否为 controller 调用的是 matchController 方法,通过四个注解( Controller、RestController、Mapping 、RequestMapping)判断一个 bean 是否为 controller,如果没找到的话,递归查找父类是否为 controller。

自定义零侵入的springboot-starter

如果是 controller 则调用 creatCglibProxy 方法,创建 cglib 的代理对象,对象用到了 ControllerEnhanceInterceptor 对象,在 ControllerEnhanceInterceptor 中实现了对当前 controller 中的所有方法做增强的逻辑。

自定义零侵入的springboot-starter

ControllerEnhanceInterceptor 对象实现了 MethodInterceptor,其实就是实现了 Advice 接口,主要的目的就是做增强,在 invoke 方法中,对 controller 方法 (Object proceed = invocation.proceed()) 调用的前后做增强。

自定义零侵入的springboot-starter


# 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-starter

通过测试,发现结果和预期的效果一致,springboot 中仅仅引入了 jar 包,就能实现相关的控制,零业务代码侵入,有了 spring-context 中的这些扩展点,对整个框架的功能可以做很多很多的扩展。

github地址 https://github.com/GITHUBFORSUMMER/spring-starter

我的个人网站 https://www.huangyingsheng.com/2021/07/10/6b23595c-5c45-e08c-d80a-7fc2582df0ba/

上一篇:解决使用SpringBoot MongoDB Starter连接MongoDB密码带有@的问题


下一篇:封装一个流水号ID生成器:id-spring-boot-starter