[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)
也就是说:
只需要使用@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 loadBeanFromContext(String beanName, Class clazz) {
return defaultListableBeanFactory.getBean(beanName, clazz);
}
@Override
public T loadBeanFromContext(Class 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
测试例子:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
@EnableRedisClient
public class JedisSingleClientRegistrarTest {
@Autowired
@Qualifier(value = "defaultRedisTemplate")
private RedisTemplate 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,在广州奋斗,白天上班,晚上和双休不定时加班,晚上有空坚持写下博客。
希望我的文章能够给你带来收获,共勉。