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,在广州奋斗,白天上班,晚上和双休不定时加班,晚上有空坚持写下博客。
希望我的文章能够给你带来收获,共勉。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容