8 -- 深入使用Spring -- 4...5 AOP代理:基于注解的“零配置”方式

本文内容纲要:8 -- 深入使用Spring -- 4...5 AOP代理:基于注解的“零配置”方式

      8.4.5 基于注解的“零配置”方式

        AspectJ允许使用注解定义切面、切入点和增强处理,而Spring框架则可识别并根据这些注解来生成AOP代理。Spring只是使用了和AspectJ 5 一样的注解,但并没有使用AspectJ的编译器或至如期,底层依赖使用的是Spring AOP,依然是在运行时动态生成AOP代理,并不依赖于AspectJ 的编译器或者织入器。

        为了启用Spring 对@AspectJ切面配置的支持,并保证Spring容器中的目标Bean被一个或多个切面自动增强,必须在Spring配置文件中配置如下片段:

<?xml version="1.0" encoding="UTF-8"?>

<!-- Spring 配置文件的根元素,使用Spring-beans-4.0.xsd语义约束 -->

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:P="http://www.springframework.org/schema/p"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-4.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-4.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 启动@AspectJ支持 -->

<aop:aspectj-autoproxy/>

</beans>

        如果不打算使用Spring的XML Schema配置方式,则应该在Spring配置文件中增加如下片段来启用@AspectJ支持:

<!-- 启动@AspectJ支持 -->

<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" />

        AnnotationAwareAspectJaAutoProxyCreator是一个Bean后处理器,该Bean后处理器将会为容器中的所有Bean生成AOP代理。

        为了在Spring应用中启动@AspectJ支持,还需要在应用的类加载路径下增加两个AspectJ库:aspectjweaver.jar和aspectjrt.jar,直接使用AspectJ安装路径下lib目录中的两个JAR文件即可。除此之外,Spring AOP还需要依赖一个aopalliance.jar。

        1.定义切面

          当启动@AspectJ支持后,只要在Spring容器中配置一个带@AspectJ注解的Bean,Spring将会自动识别该Bean,并将该Bean作为切面处理。

          提示:

            在Spring容器中配置切面Bean(即带@Aspect注解的Bean),与配置普通Bean没有任何区别,一样使用<bean.../>元素进行配置,一样支持使用依赖注入来配置属性值;如果启动了Spring的“零配置”特性,一样可以让Spring自动搜索,并加载指定路径下的切面Bean。

          使用@Aspect标注一个Java类,该Java类将会作为切面Bean:

package edu.pri.lime._8_4_5.aspect;

//使用@Aspect定义一个切面类

@Aspect

public class LogAspect {

// 定义该类的其他内容

...

}

          切面类(用@Aspect修饰的类)和其他类一样可以有方法、成员变量定义,还可能包括切入点、增强处理定义。

          当使用@Aspect来修饰一个Java类之后,Spring将不会把该Bean当成组件Bean处理,因此负责自动增强的后处理Bean将会略过该Bean,不会对该Bean进行任何增强处理。

        2.定义Before增强处理

          在一个切面类里使用@Before来修饰一个方法时,该方法将作为Before增强处理。使用@Before修饰时,通常需要指定一个value属性值,该属性值指定一个切入点表达式(既可以是一个已有的切入点,也可以直接定义切入点表达式),用于指定该增强处理将被织入哪些切入点。

          使用@Before定义一个Before增强处理:

          Class : Aspect -- @Before

package edu.pri.lime._8_4_5.before;

// 定义一个切面

@Aspect

public class AuthAspect {

// 匹配 edu.pri.lime._8_4_5.before 包下所有类的所有方法的执行作为切入点

@Before("execution(* edu.pri.lime._8_4_5.before.bean.impl.*.*(..))")

public void authority(){

System.out.println("模拟执行权限检查");

}

}

          Class : HelloImpl

package edu.pri.lime._8_4_5.before.bean.impl;

import edu.pri.lime._8_4_5.before.bean.Hello;

@Componet("hello")

public class HelloImpl implements Hello{

@Override

public void foo() {

}

@Override

public void addUser(String name, String pass) {

}

}

          XML :

<?xml version="1.0" encoding="UTF-8"?>

<!-- Spring 配置文件的根元素,使用Spring-beans-4.0.xsd语义约束 -->

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:P="http://www.springframework.org/schema/p"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-4.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-4.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 指定自动搜索Bean组件、自动搜索切面类 -->

<context:component-scan base-package="edu.pri.lime._8_4_5.before">

<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>

</context:component-scan>

<!-- 启动@AspectJ支持 -->

<aop:aspectj-autoproxy/>

</beans>

          Class : SpringTest

package edu.pri.lime._8_4_5.main;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import edu.pri.lime._8_4_5.before.bean.Hello;

public class TestSpring {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("app_8_4_5.xml");

// 不要指定类型,因为@Aspect注解会生成目标对象hello的AOP代理类,IoC容器会发生无法注入异常

Hello hello = (Hello) ctx.getBean("hello");

hello.foo();

System.out.println(hello.addUser("lime", "24"));

}

}

          Console :

模拟执行权限检查

执行Hello组件的foo()方法

模拟执行权限检查

执行Hello组件的addUser添加用户 : lime

20

          注意:

            使用@Before增强处理只能在目标方法执行之前织入增强,如果@Before增强处理没有特殊处理,目标方法总会自动执行,如果@Before处理需要阻止目标方法的执行,可通过抛出一个异常来实现。@Before增强处理执行时,目标方法还未获得执行机会,所以@Before增强处理无法访问目标方法的返回值。

        3.定义AfterReturning增强处理

          @AfterReturning 注解可修饰AfterReturning增强,AfterReturning增强处理将在目标方法正常完成后被织入。

          @AfterReturning注解可指定如如下两个常用属性:

            ⊙ pointcut/value : 这两个属性的作用是一样的,它们都用于指定切入点对应的切入表达式。一样即可是一个已有的切入点,也可直接定义切入点表达式。当指定了pointcut属性值后,value属性值将会被覆盖。

            ⊙ returning : 该属性值指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值。除此之外,在Advice方法中定义该形参(代表目标方法的返回值)时指定的类型,会限制目标方法必须返回指定类型的值或没有返回值。

          Class : Aspect -- @AfterReturning

package edu.pri.lime._8_4_5.afterReturning;

//定义一个切面

@Aspect

public class LogAspect {

// 匹配包下所有类的所有方法的执行作为切入点

@AfterReturning(pointcut = "execution(* edu.pri.lime._8_4_5.afterReturning.bean.impl.*.*(..))", returning = "rvt")

// 声明rvt时指定的类型会限制目标方法必须返回指定类型的值或没有返回值

// 此处将rvt的类型声明为Object,意味着对目标方法的返回值不加限制

public void log(Object rvt){

System.out.println("获取目标方法返回值 : " + rvt);

System.out.println("模拟记录日志功能...");

}

}

          XML :

<?xml version="1.0" encoding="UTF-8"?>

<!-- Spring 配置文件的根元素,使用Spring-beans-4.0.xsd语义约束 -->

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:P="http://www.springframework.org/schema/p"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-4.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-4.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 指定自动搜索Bean组件、自动搜索切面类 -->

<context:component-scan base-package="edu.pri.lime._8_4_5.afterReturning">

<!-- 指定自动搜索@Aspect注解的切面类 -->

<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>

</context:component-scan>

<!-- 启动@AspectJ支持 -->

<aop:aspectj-autoproxy/>

</beans>

          Class : AfterReturningTest

package edu.pri.lime._8_4_5.afterReturning;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import edu.pri.lime._8_4_5.afterReturning.bean.Hello;

public class AfterReturningTest {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("app_8_4_5_afterReturning.xml");

// 不要指定类型,因为@Aspect注解会生成目标对象hello的AOP代理类,IoC容器会发生无法注入异常

Hello hello = (Hello) ctx.getBean("hello");

hello.foo();

System.out.println(hello.addUser("lime", "23"));

}

}

          Console :

执行Hello组件的foo()方法

12:51:05.649 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'logAspect'

获取目标方法返回值 : null

模拟记录日志功能...

执行Hello组件的addUser添加用户23

获取目标方法返回值 : 22

模拟记录日志功能...

22

          注意 :

            虽然AfterReturning增强处理可以访问到目标方法的返回值,但它不可以改变目标方法的返回值。

        4.定义AfterThrowing增强处理

          @AfterThrowing 注解可修饰AfterThrowing增强处理,AfterThrowing增强处理主要用于处理程序中为处理的异常。

          @AfterThrowing 注解可指定如下两个常用属性:

            ⊙ pointcut/value : 这两个属性的作用是一样的,它们都用于指定该切入点对应的切入表达式。一样即可是一个已有的切入点,也可直接定义切入点表达式。当指定了pointcut属性值后,value属性值将会被覆盖。

            ⊙ throwing : 该属性值指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。除此之外,在Advice方法中定义该形参(代表目标方法抛出的异常)时指定的类型,会限制目标方法必须抛出指定类型的异常。

          Class : Aspect -- @AfterThrowing

package edu.pri.lime._8_4_5.afterThrowing;

//定义一个切面

@Aspect

public class RepairAspect {

// 匹配包下所有类的所有方法的执行作为切入点

@AfterThrowing(pointcut = "execution(* edu.pri.lime._8_4_5.afterThrowing.bean.impl.*.*(..))", throwing = "ex")

// 声明ex时指定的类型会限制目标方法必须抛出指定类型的异常

// 此处将ex的类型声明为Throwable,意味着对目标方法抛出的异常不加限制

public void doRecoveryActions(Throwable ex){

System.out.println("目标方法中抛出的异常 : " + ex);

System.out.println("模拟Advice对异常的修复...");

}

}

          Class : Component

package edu.pri.lime._8_4_5.afterThrowing.bean.impl;

import edu.pri.lime._8_4_5.afterThrowing.bean.Hello;

@Component("hello")

public class HelloImpl implements Hello{

@Override

public void foo() {

System.out.println("执行Hello组件的foo()方法");

}

@Override

public int addUser(String name, String pass) {

System.out.println("执行Hello组件的addUser添加用户 : " + name);

if(name.length() < 3 || name.length() > 10){

throw new IllegalArgumentException("name 参数的长度必须大于3,小于10!");

}

return 20;

}

}

          Class :

package edu.pri.lime._8_4_5.afterThrowing.main;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import edu.pri.lime._8_4_5.afterThrowing.bean.Hello;

public class AfterThrowingTest {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("app_8_4_5_afterThrowing.xml");

Hello hello = (Hello) ctx.getBean("hello");

hello.foo();

System.out.println(hello.addUser("li", "24"));

}

}

          Console :

执行Hello组件的foo()方法

执行Hello组件的addUser添加用户 : li

13:00:57.121 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'repairAspect'

目标方法中抛出的异常 : java.lang.IllegalArgumentException: name 参数的长度必须大于3,小于10!

模拟Advice对异常的修复...

Exception in thread "main" java.lang.IllegalArgumentException: name 参数的长度必须大于3,小于10!

at edu.pri.lime._8_4_5.afterThrowing.bean.impl.HelloImpl.addUser(HelloImpl.java:17)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.lang.reflect.Method.invoke(Unknown Source)

at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317)

at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)

at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:58)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)

at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)

at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)

at com.sun.proxy.$Proxy7.addUser(Unknown Source)

at edu.pri.lime._8_4_5.afterThrowing.main.AfterThrowingTest.main(AfterThrowingTest.java:16)

          注意 :

            AOP的AfterThrowing处理虽然可以对目标方法的异常进行处理,但这种处理与直接使用catch捕捉不同------catch捕捉意味着完全处理该异常,如果catch块中没有重新抛出新异常,则该方法可以正常结束;而AfterThrowing处理虽然处理了该异常,但它不能完全处理该异常,该异常依然会传播到上一级调用者(本示例程序中直接传播到JVM,故导致程序中止)。

        5.After增强处理

          @After注解修饰After增强处理,与AfterReturning增强处理类似,但是:

            ⊙ AfterReturning增强处理只有在目标方法成功完成后才会被织入。

            ⊙ After增强处理不管目标方法如何结束(包括成功完成和遇到异常中止两种情况),它都会被织入。

          因为不论一个方法是如何结束的,After增强处理都会被织入,因此After增强处理必须准备处理正常返回和异常返回两种情况,这种增强处理通常用于释放资源。

          使用@After注解时需要指定一个value属性,该属性值用于指定该增强处理被织入的切入点,即可是一个已有的切入点,也可直接指定切入点表达式。

          Class : Aspect--@After

package edu.pri.lime._8_4_5.after;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.Aspect;

@Aspect

public class ReleaseAspect {

// 匹配包下所有类的所有方法的执行作为切入点

@After("execution(* edu.pri.lime._8_4_5.after.bean.impl.*.*(..))")

public void release(){

System.out.println("模拟方法结束后的释放资源...");

}

}

          Class : HelloImpl

package edu.pri.lime._8_4_5.after.bean.impl;

import org.springframework.stereotype.Component;

import edu.pri.lime._8_4_5.after.bean.Hello;

@Component("hello")

public class HelloImpl implements Hello{

@Override

public void foo() {

System.out.println("执行Hello组件的foo()方法");

}

@Override

public int addUser(String name, String pass) {

System.out.println("执行Hello组件的addUser添加用户 : " + name);

if(name.length() < 3 || name.length() > 10){

throw new IllegalArgumentException("name 参数的长度必须大于3,小于10!");

}

return 22;

}

}

          XML :

<?xml version="1.0" encoding="UTF-8"?>

<!-- Spring 配置文件的根元素,使用Spring-beans-4.0.xsd语义约束 -->

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:P="http://www.springframework.org/schema/p"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-4.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-4.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 指定自动搜索Bean组件、自动搜索切面类 -->

<context:component-scan base-package="edu.pri.lime._8_4_5.after">

<!-- 指定自动搜索@Aspect注解的切面类 -->

<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>

</context:component-scan>

<!-- 启动@AspectJ支持 -->

<aop:aspectj-autoproxy/>

</beans>

          Class : AfterTest

package edu.pri.lime._8_4_5.after.main;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import edu.pri.lime._8_4_5.after.bean.Hello;

public class AfterTest {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("app_8_4_5_after.xml");

Hello hello = (Hello) ctx.getBean("hello");

hello.foo();

System.out.println(hello.addUser("lime", "24"));

System.out.println(hello.addUser("li", "24"));

}

}

          Console:

执行Hello组件的foo()方法

13:54:12.678 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'releaseAspect'

模拟方法结束后的释放资源...

执行Hello组件的addUser添加用户 : lime

模拟方法结束后的释放资源...

22

执行Hello组件的addUser添加用户 : li

模拟方法结束后的释放资源...

Exception in thread "main" java.lang.IllegalArgumentException: name 参数的长度必须大于3,小于10!

at edu.pri.lime._8_4_5.after.bean.impl.HelloImpl.addUser(HelloImpl.java:19)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.lang.reflect.Method.invoke(Unknown Source)

at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317)

at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)

at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:43)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)

at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)

at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)

at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)

at com.sun.proxy.$Proxy7.addUser(Unknown Source)

at edu.pri.lime._8_4_5.after.main.AfterTest.main(AfterTest.java:15)

        6.Around增强处理

          @Around 注解用于修饰Around增强处理,Around增强处理是功能比较强大的增强处理,它近似等于Before增强处理和AfterReturning增强处理的总和,Around增强处理即可在执行目标方法之前织入增强动作,也可在执行目标方法之后织入增强动作。

          与Before增强处理、AfterReturning增强处理不同的是,Around增强处理可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标方法的执行。

          Around增强处理可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。

          Around 增强处理的功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before增强处理、AfterReturning增强处理就能解决的问题,就没有必要使用Around增强处理了。如果需要目标方法执行之前和执行之后共享某种状态数据,则应该考虑使用Around增强处理;尤其是需要改变目标方法的返回值时,则只能使用Around增强处理了。

          使用@Around注解时需要指定一个value属性,该属性指定该增强处理被织入的切入点。

          当一定一个Around增强处理方法时,该方法的第一个形参必须是ProceedingJoinPoint类型(至少包含一个形参),在增强处理方法体内,调用ProceedingJoinPoint参数的Proceed()方法才会执行目标方法------这就是Around增强处理可以完全控制目标方法的执行时机、如何执行的关键;如果程序没有调用ProceedingJoinPoint参数的proceed()方法,则目标方法不会被执行。

          调用PorceedingJoinPoint参数的proceed()方法时,还可以传入一个Object[] 对象作为参数,该数组中的值将被传入目标方法作为执行方法的实参。

          Class : Aspect ------ @Around

package edu.pri.lime._8_4_5.around;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

@Aspect

public class TxAspect {

@Around("execution(* edu.pri.lime._8_4_5.around.bean.impl.*.*(..))")

public Object processTx(ProceedingJoinPoint jp) throws Throwable{

System.out.println("执行目标方法之前,模拟开始事务...");

// 获取目标方法原始的调用参数

Object[] args = jp.getArgs();

if(null != args && args.length > 1){

// 修改目标方法调用参数的第一个参数

args[0] = "【增加的前缀】" + args[0];

}

// 以改变后的参数去执行目标方法,并保存目标方法执行后的返回值

Object rvt = jp.proceed(args);

System.out.println("执行目标方法之后,模拟结束事务...");

if(rvt != null && rvt instanceof Integer){

rvt = (Integer) rvt * (Integer) rvt;

}

return rvt;

}

}

          Class : HelloImpl

package edu.pri.lime._8_4_5.around.bean.impl;

import org.springframework.stereotype.Component;

import edu.pri.lime._8_4_5.around.bean.Hello;

@Component("hello")

public class HelloImpl implements Hello{

@Override

public void foo() {

System.out.println("执行Hello组件的foo()方法");

}

@Override

public int addUser(String name, String pass) {

System.out.println("执行Hello组件的addUser添加用户 : " + name);

return 22;

}

}

          XML :

<?xml version="1.0" encoding="UTF-8"?>

<!-- Spring 配置文件的根元素,使用Spring-beans-4.0.xsd语义约束 -->

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:P="http://www.springframework.org/schema/p"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-4.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-4.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 指定自动搜索Bean组件、自动搜索切面类 -->

<context:component-scan base-package="edu.pri.lime._8_4_5.around">

<!-- 指定自动搜索@Aspect注解的切面类 -->

<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>

</context:component-scan>

<!-- 启动@AspectJ支持 -->

<aop:aspectj-autoproxy/>

</beans>

          Class : AroundTest

package edu.pri.lime._8_4_5.around.main;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import edu.pri.lime._8_4_5.around.bean.Hello;

public class AroundTest {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("app_8_4_5_around.xml");

Hello hello = (Hello) ctx.getBean("hello");

hello.foo();

System.out.println(hello.addUser("lime", "25"));

}

}

          Console :

执行目标方法之前,模拟开始事务...

执行Hello组件的foo()方法

执行目标方法之后,模拟结束事务...

执行目标方法之前,模拟开始事务...

执行Hello组件的addUser添加用户 : 【增加的前缀】lime

执行目标方法之后,模拟结束事务...

484

        7.访问目标方法的参数

          访问目标方法最简单的做法是定义增强处理方法时将第一个参数定义为JoinPoint类型,当该增强处理方法被调用时,该JoinPoint参数就代表了织入增强处理的连接点。

          JoinPoint里包含了如下几个常用的方法:

            ⊙ Object[] getArgs() : 返回执行目标方法时的参数。

            ⊙ Signature getSignature() : 返回被增强的方法的相关信息。

            ⊙ Object getTarget() : 返回被织入增强处理的目标对象。

            ⊙ Object getThis() : 返回AOP框架为目标对象生成的代理对象。

          提示 :

            当使用Around增强处理时,需要将第一个参数定义为ProceedingJoinPoint类型,该类型是JoinPoint类型的子类。

          DEMO : 定义Before、Around、AfterReturing、After四种增强处理,并分别在4中增强处理中访问被织入增强处理的目标方法、执行目标方法的参数、被织入增强处理的目标对象等。

          DEMO : Class : Aspect ------ FourAdviceTest

package edu.pri.lime._8_4_5.fouradvice;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.AfterReturning;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

@Aspect

public class FourAdviceTest {

// 定义Around增强处理

@Around("execution(* edu.pri.lime._8_4_5.fouradvice.bean.impl.*.*(..))")

public Object processTx(ProceedingJoinPoint jp) throws Throwable{

System.out.println("Around 增强 : 执行目标方法之前,模拟开始事务...");

// 访问目标方法的参数

Object[] args = jp.getArgs();

// 修改目标方法的参数---------------

// 当执行目标方法的参数存在且第一个参数是字符串时

if(null != args && args.length > 0 && args[0].getClass() == String.class){

// 修改目标方法调用参数的第一个参数

args[0] = "【增加的前缀】" + args[0];

}

// 操作目标方法的进行---------------

// 执行目标方法,并保存目标方法执行后的返回值

Object rvt = jp.proceed(args);

System.out.println("Around 增强 : 执行目标方法后,模拟结束事务...");

// 修改目标方法的返回值-------------

// 如果rvt的类型是Integer,将rvt改为它的平方

if(null != rvt && rvt instanceof Integer){

rvt = (Integer)rvt * (Integer)rvt;

}

return rvt;

}

// 定义Before增强处理

@Before("execution(* edu.pri.lime._8_4_5.fouradvice.bean.impl.*.*(..))")

public void authorith(JoinPoint jp){

System.out.println("Before 增强 : 模拟执行权限检查");

// 返回被织入增强处理的目标方法

System.out.println("Before 增强 : 被织入增强处理的目标方法为 : " + jp.getSignature().getName());

// 访问执行目标方法的参数

System.out.println("Before 增强 : 目标方法的参数为 : " + Arrays.toString(jp.getArgs()));

// 访问被增强处理的目标对象

System.out.println("Before 增强 : 被织入增强处理的目标对象为 : " + jp.getTarget());

System.out.println("Before 增强 : AOP框架为目标对象生成的代理对象为 : " + jp.getThis());

}

// 定义AfterReturning增强处理

@AfterReturning(pointcut = "execution(* edu.pri.lime._8_4_5.fouradvice.bean.impl.*.*(..))", returning = "rvt")

public void log(JoinPoint jp,Object rvt){

System.out.println("AfterReturning 增强 : 获取目标方法返回值 : " + rvt);

System.out.println("AfterReturning 增强 : 模拟记录日志功能...");

// 返回被织入增强处理的目标方法

System.out.println("AfterReturning 增强 : 被织入增强处理的目标方法为 : " + jp.getSignature().getName());

// 访问执行目标方法的参数

System.out.println("AfterReturning 增强 : 目标方法的参数为 : " + Arrays.toString(jp.getArgs()));

// 访问被增强处理的目标对象

System.out.println("AfterReturning 增强 : 被织入增强处理的目标对象为 : " + jp.getTarget());

System.out.println("AfterReturning 增强 : AOP框架为目标对象生成的代理对象为 : " + jp.getThis());

}

// 定义After增强处理

@After("execution(* edu.pri.lime._8_4_5.fouradvice.bean.impl.*.*(..))")

public void release(JoinPoint jp){

System.out.println("After 增强 : 模拟方法结束后的释放资源...");

// 返回被织入增强处理的目标方法

System.out.println("After 增强 : 被织入增强处理的目标方法为 : " + jp.getSignature().getName());

// 访问执行目标方法的参数

System.out.println("After 增强 : 目标方法的参数为 : " + Arrays.toString(jp.getArgs()));

// 访问被增强处理的目标对象

System.out.println("After 增强 : 被织入增强处理的目标对象为 : " + jp.getTarget());

System.out.println("After 增强 : AOP框架为目标对象生成的代理对象为 : " + jp.getThis());

}

}

          DEMO : Class : Main

package app_8_4_5_fouradvice.main;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import edu.pri.lime._8_4_5.fouradvice.bean.Hello;

public class FourAdviceTest {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("app_8_4_5_fouradvice.xml");

Hello hello = (Hello) ctx.getBean("hello");

hello.foo();

hello.addUser("lime", "24");

}

}

          DEMO : Console :

Around 增强 : 执行目标方法之前,模拟开始事务...

Before 增强 : 模拟执行权限检查

Before 增强 : 被织入增强处理的目标方法为 : foo

Before 增强 : 目标方法的参数为 : []

Before 增强 : 被织入增强处理的目标对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

Before 增强 : AOP框架为目标对象生成的代理对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

执行Hello组件的foo()方法

Around 增强 : 执行目标方法后,模拟结束事务...

After 增强 : 模拟方法结束后的释放资源...

After 增强 : 被织入增强处理的目标方法为 : foo

After 增强 : 目标方法的参数为 : []

After 增强 : 被织入增强处理的目标对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

After 增强 : AOP框架为目标对象生成的代理对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

AfterReturning 增强 : 获取目标方法返回值 : null

AfterReturning 增强 : 模拟记录日志功能...

AfterReturning 增强 : 被织入增强处理的目标方法为 : foo

AfterReturning 增强 : 目标方法的参数为 : []

AfterReturning 增强 : 被织入增强处理的目标对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

AfterReturning 增强 : AOP框架为目标对象生成的代理对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

Around 增强 : 执行目标方法之前,模拟开始事务...

Before 增强 : 模拟执行权限检查

Before 增强 : 被织入增强处理的目标方法为 : addUser

Before 增强 : 目标方法的参数为 : [【增加的前缀】lime, 24]

Before 增强 : 被织入增强处理的目标对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

Before 增强 : AOP框架为目标对象生成的代理对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

执行Hello组件的addUser添加用户24

Around 增强 : 执行目标方法后,模拟结束事务...

After 增强 : 模拟方法结束后的释放资源...

After 增强 : 被织入增强处理的目标方法为 : addUser

After 增强 : 目标方法的参数为 : [【增加的前缀】lime, 24]

After 增强 : 被织入增强处理的目标对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

After 增强 : AOP框架为目标对象生成的代理对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

AfterReturning 增强 : 获取目标方法返回值 : 484

AfterReturning 增强 : 模拟记录日志功能...

AfterReturning 增强 : 被织入增强处理的目标方法为 : addUser

AfterReturning 增强 : 目标方法的参数为 : [【增加的前缀】lime, 24]

AfterReturning 增强 : 被织入增强处理的目标对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

AfterReturning 增强 : AOP框架为目标对象生成的代理对象为 : edu.pri.lime._8_4_5.fouradvice.bean.impl.HelloImpl@5ef6ae06

          Spring AOP 采用和AspectJ 一样的优先顺序来织入增强处理:在“进入”连接点时,具有最高优先级的增强处理将先被织入(所以在给定的两个Before增强处理中,优先级高的那个会先执行)。在“退出”连接点时,具有最高优先级的增强处理会最后被织入(所以在给定的两个After增强处理中,优先级高的那个会后执行)。

          当不同切面里的两个增强处理需要在同一个连接点被织入时,Spring AOP将以随机的顺序来织入这两个增强处理。

          如果应用需要指定不同切面类里增强处理力的优先级,Spring提供了如下两中解决方案:、

            ⊙ 让切面类实现org.springframework.core.Ordered接口,实现该接口只需实现一个int getOrder()方法,该方法的返回值越小,则优先级越高。

            ⊙ 直接使用@Order注解来修饰一个切面类,使用@Order注解时可指定一个int型的value属性,该属性值越小,则优先级越高。

          同一个切面类里的两个相同类型的增强处理在同一个连接点被织入时,Spring AOP将以随机的顺序来织入这两个增强处理,程序没有办法控制它们的织入顺序。如果确实需要保证它们以固有的顺序被织入,则可以考虑将国歌争强处理压缩成一个增强处理;或者将不同的增强处理重构到不同的切面类中,通过在切面类级别上进行排序。

          如果只需要访问目标方法的参数,Spring还提供了一种更简单的方法:可以在程序中使用args切入点表达式来绑定目标方法的参数。如果在一个args表达式中指定了一个或多个参数,则该切入点将只匹配具有对应形参的方法,且目标方法的参数值将被传入增强处理方法。

          DEMO :

package edu.pri.lime._8_4_5.argsExpression;

import org.aspectj.lang.annotation.AfterReturning;

import org.aspectj.lang.annotation.Aspect;

@Aspect

public class AccessArgAspect {

// 下面的args(arg0,arg1)会限制目标方法必须有两个形参

@AfterReturning(pointcut = "execution(* edu.pri.lime._8_4_5.argsExpression.bean.impl.*.*(..)) && args(arg0,arg1)", returning = "rvt")

// 此处指定arg0、arg1为String类型。

// 则args(arg0,arg1)还要求目标方法的两个形参都是String类型

public void access(Object rvt, String arg0, String arg1){

System.out.println("调用目标方法第1个参数为 : " + arg0);

System.out.println("调用目标方法第2个参数为 : " + arg1);

System.out.println("获取目标方法的返回值 : " + rvt);

System.out.println("模拟记录日志功能...");

}

}

          Console :

执行Hello组件的foo()方法

执行Hello组件的addUser添加用户 : lime

10:12:44.603 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'accessArgAspect'

调用目标方法第1个参数为 : lime

调用目标方法第2个参数为 : 24

获取目标方法的返回值 : 22

模拟记录日志功能...

          使用args表达式有如下两个作用:

            ⊙ 提供了一种简单的方式来访问目标方法的参数。

            ⊙ 对切入表达式增加额外的限制。

          注意args表达式括号中的两个点,它表示可匹配更多参数------如果args表达式对应的增强处理方法签名为:

@AfterReturning(pointcut = "execution(* edu.pri.lime._8_4_5.argsExpression.bean.impl.*.*(..)) && args(food,age,..)", returning = "rvt")

public void doSomething(String name,int age,Date birth)

          这意味着只要目标方法的第一个参数是String类型,第二个参数是int类型,则该方法就可匹配该切入点。

        8.定义切入点

          AspectJ 和 Spring 都允许定义切入点。所谓定义切入点,其实质就是为一个切入点表达式起一个名称,从而允许在多个增强处理中重用该名称。

          Spring AOP只支持将Spring Bean的方法执行作为连接点,所以可以把切入点看成所有能和切入点表达式匹配的Bean方法。

          切入点定义包含两个部分:

            ⊙ 一个切入点表达式

            ⊙ 一个包含名字和任意参数的方法签名。

          其中切入点表达式用于指定该切入点和那些方法进行匹配,包含名字和任意参数的方法签名将作为该切入点的名称。

          在@Aspect风格的AOP中,切入点签名采用一个普通的方法定义(方法体通常为空)来提供,且该方法的返回值必须为void;切入点表达式需要使用@Pointcut注解来标注。

/*

* 程序可以多次重复使用该切入点,甚至可以在其他切面类、其他包的切面类里使用该切入点,、

* 至于是否可以在其他切面类、其他包的切面类里访问该切入点,则取决于该方法签名前的访问控制符

*/

// 使用@Pointcut注解定义切入点

@Pointcut("execution(* transfer(..))")

// 使用一个返回值为void、方法体为空的方法来命名切入点

private void anyOldTransfer(){}

          如果需要使用本切面类中的切入点,则可在使用@Before、@After、@Around等注解定义Advice时,使用pointcut或value属性值引用已有的切入点。

@AfterReturning(pointcut = "anyOldTransfer()", returning = "rvt")

public void transfer(String newName,String oldName){

}

          如果需要使用其他切面类中的切入点,则其他切面类中的切入点不能使用private修饰。而且在使用@Before、@After、@Around等注解中的pointcut或value属性值引用已有的切入点时,必须添加类名前缀。

          Class : SystemArchitecture

package edu.pri.lime._8_4_5.pointcut;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

@Aspect

public class SystemArchitecture {

@Pointcut("execution(* edu.pri.lime._8_4_5.pointcut.bean.impl.*.updateUser(..))")

public void myPointcut(){}

}

          Class : LogAspect

package edu.pri.lime._8_4_5.pointcut;

import org.aspectj.lang.annotation.AfterReturning;

import org.aspectj.lang.annotation.Aspect;

@Aspect

public class LogAspect {

// 直接使用SystemArchitecture切面类的myPointcut()切入点

@AfterReturning(pointcut = "SystemArchitecture.myPointcut()", returning = "rvt")

public void log(Object rvt){

System.out.println("获取目标方法返回值 : " + rvt);

System.out.println("模拟记录日志功能...");

}

}

          Xml :

<?xml version="1.0" encoding="UTF-8"?>

<!-- Spring 配置文件的根元素,使用Spring-beans-4.0.xsd语义约束 -->

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:P="http://www.springframework.org/schema/p"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-4.0.xsd

http://www.springframework.org/schema/aop

http://www.springframework.org/schema/aop/spring-aop-4.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 指定自动搜索Bean组件、自动搜索切面类 -->

<context:component-scan base-package="edu.pri.lime._8_4_5.pointcut">

<!-- 指定自动搜索@Aspect注解的切面类 -->

<context:include-filter type="annotation" expression="org.aspectj.lang.annotation.Aspect"/>

</context:component-scan>

<!-- 启动@AspectJ支持 -->

<aop:aspectj-autoproxy/>

</beans>

          Class : PointcutTest

package edu.pri.lime._8_4_5.pointcut.bean.impl;

import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import edu.pri.lime._8_4_5.pointcut.bean.Hello;

public class PointcutTest {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("app_8_4_5_pointcut.xml");

Hello hello = (Hello) ctx.getBean("hello");

hello.foo();

System.out.println(hello.addUser("lime", "25"));

System.out.println(hello.updateUser("Oracle","24"));

}

}

          Console :

执行Hello组件的foo()方法

执行Hello组件的addUser添加用户 : lime

20

执行Hello组件的updateUser添加用户 : Oracle

13:30:33.502 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'logAspect'

获取目标方法返回值 : true

模拟记录日志功能...

true

        9.切入点指示符

          execution 就是一个切入点指示符。

          Spring AOP 仅支持部分AspectJ的切入点指示符,但Spring AOP还额外支持一个bean切入点指示符。因为Spring AOP只支持使用方法调用作为连接点,所以Spring AOP的切入点指示符仅匹配方法执行的连接点。

          注意:

            完成的AspectJ切入点语言支持大量的切入点指示符,但是Spring并不支持它们。Spring AOP不支持的切入点指示符有call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this和@withincode。一旦在Spring AOP中使用这些指示符,将会导致抛出IllegalArgumentException异常。

          Spring AOP 一共支持如下几种切入点指示符:

            ⊙ execution : 用于匹配执行方法的连接点,这是Spring AOP中最主要的切入点指示符。

              execution表达式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

              ⊙ modifiers-pattern : 指定方法的修饰父,支持通配符,该部分可省略。

              ⊙ ret-type-pattern : 指定方法的返回值类型,支持通配符,可以使用“*”通配符来匹配所有的返回值类型。

              ⊙ declaring-type-pattern : 指定方法所属的类,支持通配符,该部分可省略。

              ⊙ name-pattern : 指定匹配指定的方法名,支持通配符,可以使用“*”通配符来匹配所有方法。

              ⊙ param-pattern : 指定方法声明中的形参列表,支持两个通配符,即“*”和“..” ,其中“*”代表一个任意类型的参数,而“..”代表零个或多个任意类型的参数。例如:()匹配了一个不接受任何参数的方法,而(..)匹配了一个接收任意数量参数的方法(零个或更多),(*)匹配了一个接收一个任何类型参数的方法,(*,String)匹配一个接收两个参数的方法,第一个可以是任意类型,第二个则必须是String类型。

              ⊙ throws-pattern : 指定方法声明抛出的异常,支持通配符,该部分可省略。

            DEMO :

//    匹配任意public 方法的执行

execution(public * * (..))

// 匹配任何方法名以“set”开始的方法的执行

execution(* set* (..))

// 匹配AccountServiceImpl中任意方法的执行

execution(* edu.pri.lime._8_4_5.execution.bean.impl.AccountServiceImpl.*(..))

// 匹配edu.pri.lime.bean._8_4_5.execution.bean.impl.包中任意类的任意方法的执行

execution(* edu.pri.lime._8_4_5.execution.bean.impl.*.*(..))

            ⊙ within : 用于限定匹配特定类型的连接点,当使用Spring AOP的时候,只能匹配方法执行的连接点。

//    在edu.pri.lime_8_4_5.execution包中的任意连接点(在Spring AOP中只有方法执行的连接点)

within(edu.pri.lime._8_4_5.execution.*)

// 在edu.pri.lime_8_4_5.execution包或其子包中的任意连接点(在Spring AOP中只是方法执行的连接点)

within(edu.pri.lime._8_4_5.execution..*)

            ⊙ this : 用于限定AOP代理必须指定类型的实例,匹配该对象的所有连接点。当使用Spring AOP的时候,只能匹配方法执行的连接点。

//    匹配实现了edu.pri.lime._8_4_5.execution.service.AccountService接口的AOP代理的所有连接点

// 在Spring AOP中只是方法执行的连接点

this(edu.pri.lime._8_4_5.execution.service.AccountService)

            ⊙ target : 用于限定目标对象必须是指定类型的实例,匹配该对象的所有连接点。当使用Spring AOP的时候,只能匹配方法执行的连接点。

//    匹配实现了edu.pri.lime._8_4_5.execution.service.AccountService接口的目标对象的所有连接点

// 在Spring AOP 中只是方法执行的连接点、

target(edu.pri.lime._8_4_5.execution.service.AccountService)

            ⊙ args : 用于对连接点的参数类型进行限制,要求参数类型是指定类型的实例。当使用Spring AOP的时候,只能匹配方法执行的连接点。

//    匹配只接受一个参数,且传入的参数类型是Serializable的所有连接点

// 在Spring AOP中只是方法执行的连接点

args(java.io.Serializable)

            注意:

              该示例中给出的切入点表达式与execution(* *(java.io.Serializable))不同:args版本只匹配动态运行时传入的参数值是Serializable类型的情形;而execution版本则匹配方法签名只包含一个Serializable类型的形参的方法。

          Spring AOP 还提供了一个名为bean的切入点指示符,它用于限制只匹配指定Bean实例内的连接点。当然,Spring AOP中只能使用方法执行作为连接点。

            ⊙ bean : 用于限定只匹配指定Bean实例内的连接点,实际上只能使用方法执行作为连接点。定义bean表达式时需要传入Bean的id或name,表示只匹配该Bean实例内的连接点。支持使用“*”通配符。

//    匹配tradeService Bean实例内方法执行的连接点

bean(tradeService)

// 匹配名字以Service结尾的Bean实例内方法执行的连接点

bean(*Service)

          bean切入点表达式是Spring AOP额外支持的,并不是AspectJ所支持的切入点指示符。它可以明确指定为Spring的哪个Bean织入增强处理。

        10.组合切入点表达式

          Spring支持使用如下三个逻辑运算符来组合切入点表达式:

            ⊙ && : 要求连接点同时匹配来那个切入点表达式。

            ⊙ || : 只要连接点匹配任意一个切入点表达式。

            ⊙ ! : 要求连接点不匹配指定的切入点表达式。

啦啦啦

啦啦啦

啦啦啦

啦啦啦

啦啦啦

啦啦啦

啦啦啦

啦啦啦            

啦啦啦

啦啦啦

本文内容总结:8 -- 深入使用Spring -- 4...5 AOP代理:基于注解的“零配置”方式

原文链接:https://www.cnblogs.com/ClassNotFoundException/p/6426617.html

以上是 8 -- 深入使用Spring -- 4...5 AOP代理:基于注解的“零配置”方式 的全部内容, 来源链接: utcz.com/z/296204.html

回到顶部