原理參考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