案例分析
当需要为一个程序添加日志记录时,比如要记录下面方法的输入输出。
1 | public class Calculator {//计算器对象 |
2 | public int add(int i, int j) {//加法 |
3 | System.out.println(i+","+j); |
4 | int result=i+j; |
5 | System.out.println("结果:"+result); |
6 | return result; |
7 | } |
8 | } |
9 | |
10 | public void testC(){ |
11 | Calculator cal=new Calculator(); |
12 | cal.add(2,2); |
13 | } |
假装以sout()来记录,这样的代码将日志记录与业务逻辑写在一起,耦合度太高,而且,当方法很多时,要一个一个加记录,很不方便,所以使用动态代理的方式来解决这一问题。
动态代理(JDK)
newProxyInstance()创建动态代理对象
新建一个代理对象CalculatorProxy
1 | public class CalculatorProxy { |
2 | public static Calculator getProxy(final Calculator calculator){ |
3 | ClassLoader classLoader=calculator.getClass().getClassLoader(); |
4 | //被代理对象的类加载器 |
5 | Class<?>[] interfaces=calculator.getClass().getInterfaces(); |
6 | //被代理对象实现的接口 |
7 | InvocationHandler h=new InvocationHandler() { |
8 | //方法执行器 |
9 | |
10 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
11 | //proxy:代理对象,千万别动 |
12 | //method:目标对象的方法 |
13 | //args:方法调用时传的参数 |
14 | //利用反射执行目标方法 |
15 | Object result = method.invoke(calculator, args); |
16 | //目标方法执行完的返回值 |
17 | return result; |
18 | } |
19 | }; |
20 | Object proxyInstance = Proxy.newProxyInstance(classLoader, interfaces, h); |
21 | return (Calculator) proxyInstance; |
22 | } |
23 | } |
接着可以修改test方法
1 | public void testC(){ |
2 | Calculator cal=new Calculator(); |
3 | Calculator proxy= CalculatorProxy.getProxy(cal); |
4 | proxy.add(3,3);//代理对象执行方法 |
5 | proxy.sub(2,1); |
6 | } |
然后要做日志记录就可以在代理对象里面做,比如Object result = method.invoke(calculator, args);的重要位置加上详细记录:
方法执行前,方法正常执行完,方法抛出异常,方法最终结束。
1 | Object result = null; |
2 | try { |
3 | System.out.println(method.getName()+"方法开始执行,参数列表为:"+ Arrays.toString(args)); |
4 | result = method.invoke(calculator, args); |
5 | System.out.println(method.getName()+"方法正常执行完毕,结果为:"+ result); |
6 | } catch (Exception e) { |
7 | System.out.println(method.getName()+"方法执行抛出异常"+e); |
8 | } finally { |
9 | System.out.println(method.getName()+"方法最终执行结束"); |
10 | } |
结果:

在做了动态代理之后,很好的完成了解耦。
最后:可以用一个工具类来做日志记录,配置4个方法对应四个需要做日志记录的地方,将sout()替换成四个方法。
1 | //单独抽出一个工具类专门做日志记录 |
2 | public class LogUtil { |
3 | public static void start(Method method,Object... args){ |
4 | System.out.println(method.getName()+"方法开始执行,参数列表为:"+ Arrays.toString(args)); |
5 | } |
6 | public static void stop(Method method,Object result){ |
7 | System.out.println(method.getName()+"方法正常执行完毕,结果为:"+ result); |
8 | } |
9 | public static void exception(Method method,Exception e){ |
10 | System.out.println(method.getName()+"方法执行抛出异常"+e.getCause()); |
11 | } |
12 | public static void end(Method method){ |
13 | System.out.println(method.getName()+"方法最终执行结束"); |
14 | } |
15 | } |
16 | //修改代理对象执行方法的日志记录 |
17 | try { |
18 | LogUtil.start(method,args); |
19 | result = method.invoke(calculator, args); |
20 | LogUtil.stop(method,result); |
21 | } catch (Exception e) { |
22 | LogUtil.exception(method,e); |
23 | } finally { |
24 | LogUtil.end(method); |
25 | } |
产生的问题
写起来麻烦
是真的麻烦。
对象必须实现接口
如果对象没有实现任何接口,则无法创建代理对象。
代理对象和被代理对象唯一的关联就是视线里同样的接口。
所以Spring实现了AOP
一行代码都不写创建动态代理,不要求代理对象实现接口。
AOP(面向切面编程)
程序运行期间,将某段代码动态切入到指定方法的指定位置并运行。
一些概念:
以计算器日志为例。
计算器的计算方法四个日志记录点,称之为横切关注点;
在记录点做记录的方法,称之为通知方法;
通知方法属于切面类;
每一个方法所有的记录点统称为连接点;
有的点位不需要做记录,比如加减法的异常点,不会抛出异常;有的点位需要做记录,比如除法里除数为0会抛出异常,所以在方法执行抛出异常点,这样需要做记录的点位称之为切入点;
通过什么方法判定一个点位是不是切入点,这样的方法称之为切入点表达式
导坐标
1 | <dependency> |
2 | <groupId>org.springframework</groupId> |
3 | <artifactId>spring-aspects</artifactId> |
4 | <version>5.2.3.RELEASE</version> |
5 | </dependency> |
6 | <dependency> |
7 | <groupId>cglib</groupId> |
8 | <artifactId>cglib</artifactId> |
9 | <version>3.3.0</version> |
10 | </dependency> |
11 | <dependency> |
12 | <groupId>org.aspectj</groupId> |
13 | <artifactId>aspectjweaver</artifactId> |
14 | <version>1.9.5</version> |
15 | </dependency> |
16 | <dependency> |
17 | <groupId>aopalliance</groupId> |
18 | <artifactId>aopalliance</artifactId> |
19 | <version>1.0</version> |
20 | </dependency> |
存入IoC容器
给目标类和切面类加上@Component注解
给切面类加上@Aspect注解
托付给Spring
告知Spring,每个方法在何时何地运行
几个注解
@Before:前置通知,方法运行前
@AfterReturning:返回通知,方法正常返回之后
@AfterThrowing:异常通知,方法抛出异常之后
@After:后置通知,方法最终结束
@Around:环绕通知,
以@Before为例:
@Before(“execution(访问修饰符 返回值类型 方法签名)”)
1 | ("execution(public int com.czw.bean.impl.MyCalculator.add(int,int))") |
2 | //将add改成*表示该类下同修饰符,同返回值,同参数列表的所有方法 |
开启基于注解的AOP
导入aop名称空间
1 | xmlns:aop="http://www.springframework.org/schema/aop" |
2 | <!--将下面两句加入xsi:schemaLocation--> |
3 | http://www.springframework.org/schema/aop |
4 | http://www.springframework.org/schema/aop/spring-aop.xsd |
测试
1 | |
2 | private ICalculator myCalculator; |
3 | //注入bean |
4 | |
5 | public void testC(){ |
6 | myCalculator.add(2,2); |
7 | } |
结果完美
细节
aop底层就是动态代理,保存在容器中的是代理对象
1<对象类型

表面看起来是注入的bean对象,实际类型是代理对象类型。
2<不用实现接口
MyCalculator没有实现Calculator接口,也可以做到动态代理,容器中的类型就是本类型,cglib会创建实现类所有方法的内部类,最后还是一个代理对象。
3<切入点表达式写法
execution(访问修饰符 返回值类型 方法全类名(参数列表))
execution(public int com.czw.bean.impl.MyCalculator.add(int,int))
这是最精确的表示,还可以使用通配符模糊匹配
“ * ” :匹配一个或多个字符/任意一个参数/任何类型返回值
execution(public * com.*.bean.impl.MyCal*.*(int,*))
任意修饰符不能用*表示,不写就是任意修饰符(好像只要public可以选)
如果只有一个*,那就是代表整个工程下所有类所有方法
execution(* *(..)) :这个就是全方位封锁打击
“ .. “ :匹配任意多个任意类型参数/任意多层路径
execution(public int com..MyCal*.*(..))
**” && 、||、! “ **:同时满足,满足任意,不满足
4<通知方法执行顺序
正常执行时:@Before–>@After–>@AfterReturning
异常执行时: @Before–>@After–>@AfterThrowing
5<获取方法详细信息
在通知方法参数上写一个JoinPoint,表示方法详细信息
1 | ("execution(public int com.czw.bean.impl.MyCalculator.*(int,int))") |
2 | public static void start(JoinPoint joinPoint){ |
3 | Object[] args = joinPoint.getArgs(); |
4 | //获取输入参数 |
5 | Signature signature = joinPoint.getSignature(); |
6 | //signature表示方法前面,和execution里内容一样,包括修饰符、返回值、全类名、方法名、参数列表。 |
7 | System.out.println(signature.getName()+"方法开始执行,参数列表为:"+ Arrays.toString(args)); |
8 | //System.out.println("方法开始执行前"); |
9 | } |
10 | //获取方法返回值,用通知注解的returning属性可以指定参数列表里的参数为返回值 |
11 | (value="execution(xxx)",returning="result") |
12 | public static void stop(JoinPoint joinPoint,Object result){ |
13 | Signature signature = joinPoint.getSignature(); |
14 | System.out.println(signature.getName()+"方法正常执行完毕,结果为:"+ result); |
15 | //System.out.println("方法正常执行完成"); |
16 | } |
17 | //异常同理用throwing指定 |
6<Spring对通知方法不严格
唯一要求就是方法的参数列表不能乱写,通知方法是Spring利用反射调用,所以参数列表必须明确,要让Spring知道是什么。接受异常和返回值范围要写大。
7<抽取可重用切入点表达式
先声明一个没有实现的void空方法,加上@Pointcut注解,加上表达式,其他用到表达式的地方用方法名代替即可。
1 | ("execution(xxx)") |
2 | public void point(); |
3 | |
4 | ("point()") |
5 | public ...{ |
6 | ... |
7 | } |
8<环绕通知
Spring中最强大的通知,基本就是动态代理,等于其他四合一。
1 | ("point()") |
2 | public Object myAround(ProceedingJoinPoint pjp) throws Throwable { |
3 | //pjp是JoinPoint的子接口,继承了所有方法 |
4 | Object[] args = pjp.getArgs(); |
5 | String name = pjp.getSignature().getName(); |
6 | Object proceed = null; |
7 | try { |
8 | System.out.println("Around-Before:"+name+"方法开始执行,参数为"+Arrays.toString(args)); |
9 | proceed = pjp.proceed(args); |
10 | //这一句直接就是method.invoke(obj,args);要加通知在这句周围加就行 |
11 | System.out.println("Around:-AfterReturning"+name+"方法正常执行完毕,返回值:"+proceed); |
12 | } catch (Exception e) { |
13 | System.out.println("Around:-AfterThrowing"+name+"方法抛出异常,异常原因:"+e.getCause()); |
14 | } finally { |
15 | System.out.println("Around-After:"+name+"方法最终执行结束"); |
16 | } |
17 | return proceed;//这个直接就是方法返回值 |
18 | } |

十分顺利,并且顺序是正确的。
9<环绕通知与其他共存时
环绕通知执行顺序优先其他通知
环绕前置&普通前置–>目标方法–>环绕返回/异常–>环绕后置–>普通返回/异常–>普通后置
注意:出现异常时,环绕通知要把异常抛出去,不然普通通知就捕捉不到异常了
10<多切面执行顺序
没有环绕通知时,类似于层层包装,从一端到令一端,中间是目标方法,先执行切面A的前置,然后切面B前置,然后目标方法,然后切面B正常/异常,后置,然后切面A…具体哪个在外边由切面类首字母排序A最外面,Z最里面。
@Order注解加上一个int类型值,可以指定优先级,越小越优先,在最外层
当外切面加了环绕通知时,也只是在当前切面位置 贴着内切面,而不是贴着目标方法。
AOP使用场景
日志记录,权限验证,事务控ZHI
基于XML的AOP
加入容器
用bean标签配上id和class(切面类+目标类)
1 | <bean id="LogUtil" class="xxx"></bean> |
2 | <bean id="MyCalculator" class="xxx"></bean> |
相当于加上@Component注解
告知Spring切面类
使用aop名称空间指定
1 | <aop:config> |
2 | <aop:aspect ref="LogUtil"></aop:aspect> |
3 | ... |
4 | </aop:config> |
相当于加上@Aspect注解
配置运行时间和位置
加入到< aop:config >标签的< aop:aspect >中,aspect标签也有order属性
1 | <aop:pointcut id="mypoint" expression="execution(xxx)"/> |
2 | <!--声明在aspect标签里直供本切面使用,要想所有切面使用,可以声明在config标签里--> |
3 | <aop:before method="start" pointcut="execution(xxx)"/> |
4 | <!--直接写表达式--> |
5 | <aop:afterreturning method="stop" pointcut-ref="mypoint" returning="result"/> |
6 | <!--引用抽取出来的表达式,并指定返回值/异常对应方法的参数--> |
7 | <aop:afterthrowing method="exception" pointcut-ref="mypoint" throwing="exception"/> |
8 | <aop:after method="end" pointcut="execution(xxx)"/> |
9 | <aop:around method="myAround" pointcut-ref="mypoint" /> |
注意:在XML配置文件中,配置的顺序决定了通知的顺序,先配的切面在外层,后配的切面在里层。并且,切面中环绕如果先配,环绕前置在前面,环绕如果后配,普通前置在前面。
对比
注解:快速方便
XML:功能完善
重要的用配置,不重要的用XML
总结
头发-1