Spring自定義註解定義AOP配置去xml

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章