2017-5-14编程式注册advisor

[TOC]

Spring学习日常笔记2017-5-14

Q1.使用@ConfigurationProperties注解的POJO类在什么时候注册为Spring容器的Bean?

A1: 先看一个注解 @EnableConfigurationProperties

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {

    /**
     * Convenient way to quickly register {@link ConfigurationProperties} annotated beans
     * with Spring. Standard Spring Beans will also be scanned regardless of this value.
     * @return {@link ConfigurationProperties} annotated beans to register
     */
    Class<?>[] value() default {};

}

玄机就在EnableConfigurationPropertiesImportSelector这个类里面,就是看起来令人熟悉的ImportBeanDefinitionRegistrar接口实现。如果对这个钩子接口不熟悉可以翻看一下这篇博客:

Spring钩子方法和钩子接口的使用详解

主要涉及到两个名字很长的类:

1.静态内部类:ConfigurationPropertiesBeanRegistrar

2.ConfigurationPropertiesBindingPostProcessorRegistrar

前者通过获取@ConfigurationProperties的value属性(是一个Class<?>数组),获取了指定的POJO类数组,然后把这些POJO类注册为Spring容器的Bean,beanName是使用了@ConfigurationProperties的prefix属性 + "-" + type.getName( )组成,例如"spring.redis-org.xxxx.xxx.RedisProperties"(spring-boot-starter-data-redis里面的RedisProperties最终注册的beanName),然后就可以使用@Autowired注入指定的POJO以获取属性;后者是一个Bean的后处理器和注册器,主要是为了注册ConfigurationPropertiesBindingPostProcessor,这个processor是配置文件属性和POJO绑定的关键类。
关键源代码片段(来源于ConfigurationPropertiesBeanRegistrar)

Paste_Image.png

也就是说:
只需要使用@EnableConfigurationProperties注解,value指定我们需要让它成为容器的Bean的POJO类(POJO类需要使用@ConfigurationProperties注解)就可以把这些POJO注册为可以注入的Bean,然后获取到需要的属性。

@EnableConfigurationProperties这个注解的作用就是让@ConfigurationProperties注解标注的POJO注册为Spring容器的Bean。

做个测试:

定义POJO:

@ConfigurationProperties(prefix = "test")
public class CustomProp {

    private Integer age;
    private String name;


    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

定义一个注解(不用理会JedisSingleClientRegistrar):

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({JedisSingleClientRegistrar.class})
@EnableConfigurationProperties(value = CustomProp.class)
public @interface EnableRedisClient {

}

yaml文件:

test:
    name: throwable
    age: 24

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
@EnableRedisClient
public class JedisSingleClientRegistrarTest {
    
    @Autowired
    private CustomProp customProp;

    @Test
    public void testProp() throws Exception {
        System.out.println("age : " + customProp.getAge());
        System.out.println("name : " + customProp.getName());
    }
}

控制台输出:

age : 24
name : throwable

Q2.如何编程式动态注册一个Spring的advisor?

A2:在解决这个问题之前,先解决:如果编程式注册一个Bean。其实方式有很多,不过想到的暂时有下面几种:

1.使用@Bean(严格来说虽然是编程式,但是不是动态,这是Spring相对于XML配置提供的一种方式)

2.使用BeanFactory的接口实现(一般是DefaultListableBeanFactory)进行Bean注册等操作

3.使用ImportBeanDefinitionRegistrar钩子接口

4.使用BeanDefinitionRegistryPostProcessor钩子接口

当然还有其他方式,这里不详细挖掘。

方式1应该大家比较熟悉,而方式4和3大概一致,这里用方式2、3作为例子。

方式2:DefaultListableBeanFactory这个BeanFactory接口的最底层的实现具有了上层所有父类的所有方法,而且它是内建注册好的Bean,可以直接自动注入使用:

Service
public class DefaultBeanRegisterHandler implements BeanRegisterHandler {

    @Autowired
    private DefaultListableBeanFactory defaultListableBeanFactory;

    @Override
    public void registerBeanDefinition(BeanDefinitionComponent component) {
        BeanDefinition beanDefinition = BeanRegisterComponentFactory.processBeanDefinitionComponent(component);
        defaultListableBeanFactory.registerBeanDefinition(component.getBeanName(), beanDefinition);
    }

    @Override
    public Class<?> loadContextClass(String className) {
        try {
            return ClassUtils.getDefaultClassLoader().loadClass(className);
        } catch (ClassNotFoundException e) {
            throw new BeanRegisterHandleException(e);
        }
    }

    @Override
    public Object loadBeanFromContext(String beanName) {
        return defaultListableBeanFactory.getBean(beanName);
    }

    @Override
    public <T> T loadBeanFromContext(String beanName, Class<T> clazz) {
        return defaultListableBeanFactory.getBean(beanName, clazz);
    }

    @Override
    public <T> T loadBeanFromContext(Class<T> clazz) {
        return defaultListableBeanFactory.getBean(clazz);
    }

    @Override
    public void removeBeanFromContext(String beanName) {
        if (defaultListableBeanFactory.containsBeanDefinition(beanName)) {
            defaultListableBeanFactory.removeBeanDefinition(beanName);
        }
    }

}

上面的defaultListableBeanFactory.registerBeanDefinition()就是BeanDefinition的注册方法。一般来说,我们会先判断上下文中有没有同名的Bean再注册,构造BeanDefinition使用BeanDefinitionBuilder类,最核心的属性是beanName和beanClass,其他属性见BeanDefinitionBuilder里面的链式api:

if (!defaultListableBeanFactory.containsBeanDefinition(beanName)) {
    BeanDefinition myBean = BeanDefinitionBuilder.genericBeanDefinition(MyBean.class)
                   .getBeanDefinition();
    defaultListableBeanFactory.registerBeanDefinition("myBean", beanDefinition);  
 }

注册成功后可以直接注入:

@Autowired
private MyBean myBean;

方式2:ImportBeanDefinitionRegistrar钩子接口的详细使用方式见

Spring钩子方法和钩子接口的使用详解

这个接口可以说是Spring特意为非Spring体系的第三方框架的Bean注册打造的,例如Mybatis,很多Mybatis的注解里面你会看到注解里面有一个@Import(xxxxRegistrar.class)。

使用例子:

定义一个注解@EnableRedisClient:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({JedisSingleClientRegistrar.class})
public @interface EnableRedisClient {

}

定义ImportBeanDefinitionRegistrar接口的实现JedisSingleClientRegistrar:

public class JedisSingleClientRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private static final String SPRING_REDIS_PREFIX = "spring.redis";
    private static String SPRING_REDIS_POOL_PREFIX = SPRING_REDIS_PREFIX + ".pool";
    private static final Integer DEFAULT_MAXACTIVE = 8;
    private static final Integer DEFAULT_MAXIDLE = 8;
    private static final Integer DEFAULT_MINIDLE = 0;
    private static final Integer DEFAULT_MAXWAIT = -1;
    private static final Integer DEFAULT_TIMEOUT = 0;
    private int timeOut;
    private int maxIdle;
    private int minIdle;
    private int maxActive;
    private int maxWait;

    @Override
    public void setEnvironment(Environment environment) {
        String timeOutStr = environment.getProperty(SPRING_REDIS_PREFIX + ".timeout");
        String maxIdleStr = environment.getProperty(SPRING_REDIS_POOL_PREFIX + ".max-idle");
        String minIdleStr = environment.getProperty(SPRING_REDIS_POOL_PREFIX + ".min-idle");
        String maxActiveStr = environment.getProperty(SPRING_REDIS_POOL_PREFIX + ".max-active");
        String maxWaitStr = environment.getProperty(SPRING_REDIS_POOL_PREFIX + ".max-wait");
        timeOut = null != timeOutStr ? Integer.valueOf(timeOutStr) : DEFAULT_TIMEOUT;
        maxIdle = null != maxIdleStr ? Integer.valueOf(maxIdleStr) : DEFAULT_MAXIDLE;
        minIdle = null != minIdleStr ? Integer.valueOf(minIdleStr) : DEFAULT_MINIDLE;
        maxActive = null != maxActiveStr ? Integer.valueOf(maxActiveStr) : DEFAULT_MAXACTIVE;
        maxWait = null != maxWaitStr ? Integer.valueOf(maxWaitStr) : DEFAULT_MAXWAIT;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata,
                                        BeanDefinitionRegistry beanDefinitionRegistry) {
        registerJedisConnectionFactory(beanDefinitionRegistry);
        registerDefaultRedisTemplate(beanDefinitionRegistry);
    }

    private void registerJedisConnectionFactory(BeanDefinitionRegistry beanDefinitionRegistry) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxWaitMillis(maxWait);
        BeanDefinition jedisConnectionFactory = BeanDefinitionBuilder
                .genericBeanDefinition(JedisConnectionFactory.class)
                .addConstructorArgValue(poolConfig)
                .addPropertyValue("timeout", timeOut)
                .getBeanDefinition();
        beanDefinitionRegistry.registerBeanDefinition("jedisConnectionFactory", jedisConnectionFactory);
    }

    private void registerDefaultRedisTemplate(BeanDefinitionRegistry beanDefinitionRegistry) {
        BeanDefinition redisTemplate = BeanDefinitionBuilder
                .genericBeanDefinition(RedisTemplate.class)
                .addPropertyValue("defaultSerializer", buildRedisTemplateSerializer())
                .addPropertyReference("connectionFactory","jedisConnectionFactory")
                .getBeanDefinition();
        beanDefinitionRegistry.registerBeanDefinition("defaultRedisTemplate", redisTemplate);
    }

    private Jackson2JsonRedisSerializer buildRedisTemplateSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }

}

测试例子:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
@EnableRedisClient
public class JedisSingleClientRegistrarTest {

    @Autowired
    @Qualifier(value = "defaultRedisTemplate")
    private RedisTemplate<String, String> redisTemplate;

    @Test
    public void testRedisTemplate() throws Exception {
        redisTemplate.opsForValue().set("foo", "bar");
        assertEquals(redisTemplate.opsForValue().get("foo"), "bar");
    }
}   

一般来说,你的IDE会标红"defaultRedisTemplate",原因很简单,IDE识别不了这种编程式注册Bean,但是使用是正常的。

进入正题:

构思:现在需要自定义两个注解:@EnableDynamicDataSource和@DynamicDataSource,前者用于当使用了@EnableDynamicDataSource注解后,会动态注册一个advisor,用around的方式处理@DynamicDataSource携带的value用于动态切换当前执行方法的数据源。

@EnableDynamicDataSource:

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Import({AspectJExpressionPointcutAdvisorRegistrar.class})
public @interface EnableDynamicDataSource {

}

@DynamicDataSource:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicDataSource {

    String value() default "";
}

AspectJExpressionPointcutAdvisorRegistrar:

public class AspectJExpressionPointcutAdvisorRegistrar implements ImportBeanDefinitionRegistrar {


    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                        BeanDefinitionRegistry registry) {
        Advice advice = (MethodInterceptor) invocation -> {
            ProxyMethodInvocation pmi = (ProxyMethodInvocation) invocation;
            ProceedingJoinPoint pjp = new MethodInvocationProceedingJoinPoint(pmi);
            System.out.println(pjp);
            Method method = invocation.getMethod();
            if (method.isAnnotationPresent(DynamicDataSource.class)) {
                DynamicDataSource annotation = method.getAnnotation(DynamicDataSource.class);
                String targetDataSource = annotation.value();
                System.out.println("动态切换数据源,当前数据源为:" + targetDataSource);
                System.out.println("方法放行前拦截...");
                Object obj = invocation.proceed();//方法放行
                System.out.println("方法放行后拦截...");
                return obj;
            }
            return invocation.proceed();
        };
        BeanDefinition aspectJBean = BeanDefinitionBuilder.genericBeanDefinition(AspectJExpressionPointcutAdvisor.class)
                //定义输出路径,一般是日志的输出类,方便排查问题
                .addPropertyValue("location", "$$aspectJAdvisor##")
                //定义AspectJ切点表达式
                .addPropertyValue("expression", "@annotation(org.throwable.aspectj.annotation.DynamicDataSource)")
                //定义织入的增强对象,就是上面的自定义的around类型的advice的实现
                .addPropertyValue("advice", advice)
                .getBeanDefinition();
        registry.registerBeanDefinition("aspectJAdvisor", aspectJBean);
    }
}

定义一个业务类DynamicDataSourceService模拟动态切换数据源:

@Service
public class DynamicDataSourceService {

    @DynamicDataSource(value = "master")
    public void process(){
        System.out.println("DynamicDataSourceService process!!");
    }
}

测试类:

@SpringBootTest(classes = Application.class)
@RunWith(SpringJUnit4ClassRunner.class)
@EnableDynamicDataSource
public class DynamicDataSourceServiceTest {

    @Autowired
    private DynamicDataSourceService dynamicDataSourceService;

    @Test
    public void process() throws Exception {
        dynamicDataSourceService.process();
    }

}

控制台输出:

execution(void org.throwable.aspectj.service.DynamicDataSourceService.process())
动态切换数据源,当前数据源为:master
方法放行前拦截...
DynamicDataSourceService process!!
方法放行后拦截...

见动态注册advisor验证成功。

End on 2017-5-14 19:20.
Help yourselves!
我是throwable,在广州奋斗,白天上班,晚上和双休不定时加班,晚上有空坚持写下博客。
希望我的文章能够给你带来收获,共勉。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 121,117评论 16 134
  • 什么是Spring Spring是一个开源的Java EE开发框架。Spring框架的核心功能可以应用在任何Jav...
    jemmm阅读 15,291评论 1 134
  • spring官方文档:http://docs.spring.io/spring/docs/current/spri...
    牛马风情阅读 846评论 0 3
  • 文章作者:Tyan博客:noahsnail.com 3.4 Dependencies A typical ente...
    SnailTyan阅读 3,246评论 2 7
  • 2007年秋天, 记得第一次见你是在回学校的公交车上, 当时,是你母亲带你去学打架子鼓,言谈中,得知你是我导师的儿...
    Joff_liuwen阅读 88评论 0 0