原理参考ImportBeanDefinitionRegistrar+SPI简化Spring开发
spring中AOP使用非常广泛,引入方式一般分为两种,注解方式或xml方式。直接方式使用@AspectJ这样的注解,其缺点是需要手写切面实现业务逻辑,不太方便用第三方包做切面。xml方式打破了注解方式的局限,配置起来较为灵活,但xml毕竟偏向于配置,有一定的臃肿性。换句话说,在去xml的大趋势下,如何消除用来配置AOP的xml呢?
本文提出一种解决方案,自定义配置AOP的注解,使用ImportBeanDefinitionRegistrar机制,动态解析注解,基于xml的AOP配置模板生成xml配置,借助Spring解析xml配置的工具解析aop bean定义加载到context中。
首先定义配置AOP定义的注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@EnableAutoRegistrar
@Repeatable(AopDefinitions.class)
public @interface AopDefinition {
boolean proxyTargetClass() default false;
String refBeanName();
int order() default 0;
String advice();
String adviceMethod();
/**
* 切入点,最终会以空格拼接
*/
String[] pointcut();
}
以及批量注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@EnableAutoRegistrar
public @interface AopDefinitions {
AopDefinition[] value();
}
再定义xml方式的AOP配置模板:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config proxy-target-class="${enable-proxy-target-class}">
<aop:aspect ref="${aop-ref}" order="${aop-order}">
<aop:${aop-advice} method="${aop-advice-method}"
pointcut="${aop-pointcut}"/>
</aop:aspect>
</aop:config>
</beans>
可见注解的参数就是为了匹配AOP xml模板中参数。下面是利用模板生成xml配置并解析bean以及注册到context的工具类:
@Data
@Accessors(chain = true)
public class AspectJParams {
private boolean proxyTargetClass;
private String aopRefBean;
private int aopOrder;
private String aopAdvice;
private String aopAdviceMethod;
private String aopPointcut;
public Map<String, Object> build() {
Map<String, Object> map = new HashMap<>();
map.put("enable-proxy-target-class", proxyTargetClass);
map.put("aop-ref", aopRefBean);
map.put("aop-order", aopOrder);
map.put("aop-advice", aopAdvice);
map.put("aop-advice-method", aopAdviceMethod);
map.put("aop-pointcut", aopPointcut);
return map;
}
}
@Slf4j
public class AopBeanDefinitionRegistry {
public static int loadBeanDefinitions(BeanDefinitionRegistry registry, AspectJParams params)
throws BeanDefinitionStoreException, IOException {
final ClassPathResource classPathResource = new ClassPathResource("/resource/aop-definition-template.tpl");
String template = null;
try (InputStream inputStream = classPathResource.getInputStream()) {
template = IOUtils.readFromInputStream(inputStream, Charset.forName("UTF-8"), true);
}
if (template == null || template.isEmpty()) {
throw new RuntimeException("AOP template empty");
}
String defineText = new StringSubstitutor(params.build()).replace(template);
if (log.isTraceEnabled()) {
log.debug("AOP define: \n{}", defineText);
}
ByteArrayResource byteArrayResource = new ByteArrayResource(defineText.getBytes());
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
return reader.loadBeanDefinitions(byteArrayResource);
}
}
主要是利用XmlBeanDefinitionReader来加载bean定义。
对于我们自定义注解的处理我们定义对应的Handler:
@Slf4j
public class AopDefinitionHandler implements ConfigurationRegisterHandler {
@Override
public void registerBeanDefinitions(RegisterBeanDefinitionContext context) {
Set<AnnotationAttributes> annotationAttributes = SpringAnnotationConfigUtils.attributesForRepeatable(
context.getImportingClassMetadata(), AopDefinitions.class, AopDefinition.class);
if (CollectionUtils.isEmpty(annotationAttributes)) {
return;
}
for (AnnotationAttributes attributes : annotationAttributes) {
final AspectJParams params = new AspectJParams()
.setProxyTargetClass(attributes.getBoolean("proxyTargetClass"))
.setAopRefBean(attributes.getString("refBeanName"))
.setAopOrder(attributes.getNumber("order").intValue())
.setAopAdvice(attributes.getString("advice"))
.setAopAdviceMethod(attributes.getString("adviceMethod"))
.setAopPointcut(Arrays.stream(attributes.getStringArray("pointcut"))
.filter(StringUtils::isNotEmpty)
.reduce((x, y) -> x + " " + y).get());
try {
AopBeanDefinitionRegistry.loadBeanDefinitions(context.getRegistry(), params);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public int getOrder() {
return 0;
}
}
这样我们在@Configuration的class上使用@AopDefinition或@AopDefinitions注解,即可向容器中定义AOP。示例如下:
@Configuration
@AopDefinition(proxyTargetClass = true, refBeanName = "logEnvAspect",
order = -10, advice = "around", adviceMethod = "doAspect",
pointcut = {
"@within(org.springframework.stereotype.Controller)",
"|| @annotation(org.springframework.stereotype.Controller)",
"|| @within(org.springframework.web.bind.annotation.RestController)",
"|| @annotation(org.springframework.web.bind.annotation.RestController)"
})
public class AopDefinitionConfiguration {
@Bean("logEnvAspect")
public LogEnvAspect logEnvAspect() {
return new LogEnvAspect().setKeys(Arrays.asList("system", "host", "port", "appName", "module"));
}
}
我们在@Controller或@RestController的bean方法上引入了around通知,切点逻辑是LogEnvAspect这个bean的doAspect方法。效果配下面的xml配置是一样的:
<aop:config proxy-target-class="true">
<!-- 基于注解的RedisCache -->
<aop:aspect ref="logEnvAspect" order="-10">
<aop:around method="doAspect"
pointcut="@within(org.springframework.stereotype.Controller)
|| @annotation(org.springframework.stereotype.Controller)
|| @within(org.springframework.web.bind.annotation.RestController)
|| @annotation(org.springframework.web.bind.annotation.RestController)"/>
</aop:aspect>
</aop:config>
完整代码参考github