2021-07-10

自定义零侵入的springboot-starter


背景:

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

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

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

分析

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

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

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


image


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

  • 第一步:我们需要在 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

image

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

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

image

在 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 接口的 postProcessAfterIniti......

原文转载:http://www.shaoqun.com/a/861382.html

跨境电商:https://www.ikjzd.com/

bsci 认证:https://www.ikjzd.com/w/2339

敦煌网:https://www.ikjzd.com/w/189

国际标准书号:https://www.ikjzd.com/w/174


背景:突发奇想,有没有什么办法可以不需要在springboot的启动类上添加类似@EnableEurekaClient、@EnableFeignClients、@EnableXXXXXXX这样的注解,也不需要在代码里添加@Configuration类似的配置类,更不需要修改原有的代码,仅需在pom中引入一个jar包,然后什么都不用做就能对项目的运行产生影响,或者随意支配。想了下,要不就拿所有的co
upc:https://www.ikjzd.com/w/111
史上最最疯狂的年终亚马逊旺季战役打响,用好广告锦囊,Q4收割一整年KPI!:https://www.ikjzd.com/articles/132122
卖家爆哭!遇上亚马逊"真Reviewer",一夜损失上千刀!:https://www.ikjzd.com/articles/132121
谷歌营销:如何利用目标客户匹配工具挖掘潜在用户:https://www.ikjzd.com/articles/132116
海运价格飞涨3倍冲上热搜 高价可能持续到2021年:https://www.ikjzd.com/articles/132178
强行扒开女班长大腿 班长让我脱了她的内衣:http://www.30bags.com/m/a/249773.html
同房交换4P好爽 口述做爰全过程和细节:http://www.30bags.com/m/a/249814.html
被两个男人抬起腿做 两根粗大一前一后好深:http://www.30bags.com/m/a/249831.html
为什么当我和我可爱的女朋友左爱在一起时,我会感到内疚:http://lady.shaoqun.com/a/411978.html
女朋友矮,男生有什么样的体验?(漫画):http://lady.shaoqun.com/a/411980.html
四个初中生在洗浴城花了4000多块钱,一大早穿着拖鞋溜了:http://lady.shaoqun.com/a/411979.html
有一个娇小的老婆是什么感觉?网友:找到我腰间盘突出的原因:http://lady.shaoqun.com/a/411981.html

No comments:

Post a Comment