AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
一、为什么需要代理模式
假设需实现一个计算的类Math、完成加、减、乘、除功能,如下所示:
package cn.liuw.aop;
public class Math {
//加
public int add(int n1,int n2){
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
return result;
}
//减
public int sub(int n1,int n2){
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
return result;
}
//乘
public int mut(int n1,int n2){
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
return result;
}
//除
public int div(int n1,int n2){
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
return result;
}
}
现在需求发生了变化,要求项目中所有的类在执行方法时输出执行耗时。最直接的办法是修改源代码,如下所示:
package cn.liuw.aop;
import java.util.Random;
public class Math {
//加
public int add(int n1,int n2){
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//减
public int sub(int n1,int n2){
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//乘
public int mut(int n1,int n2){
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//除
public int div(int n1,int n2){
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//模拟延时
public void lazy()
{
try {
int n=(int)new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试运行:
package cn.liuw.aop;
public class Test {
@org.junit.Test
public void test01()
{
Math math=new Math();
int n1=100,n2=5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
}
运行结果:
缺点:
1、工作量特别大,如果项目中有多个类,多个方法,则要修改多次。
2、违背了设计原则:开闭原则(OCP),对扩展开放,对修改关闭,而为了增加功能把每个方法都修改了,也不便于维护。
3、违背了设计原则:单一职责(SRP),每个方法除了要完成自己本身的功能,还要计算耗时、延时;每一个方法引起它变化的原因就有多种。
4、违背了设计原则:依赖倒转(DIP),抽象不应该依赖细节,两者都应该依赖抽象。而在Test类中,Test与Math都是细节。
使用静态代理可以解决部分问题。
二、静态代理
1、定义抽象主题接口
package cn.liuw.aop;
/**
* 接口
* 抽象主题
*/
public interface IMath {
//加
int add(int n1, int n2);
//减
int sub(int n1, int n2);
//乘
int mut(int n1, int n2);
//除
int div(int n1, int n2);
}
2、主题类,算术类,实现抽象接口
package cn.liuw.aop;
/**
* 被代理的目标对象
*真实主题
*/
public class Math implements IMath {
//加
public int add(int n1,int n2){
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
return result;
}
//减
public int sub(int n1,int n2){
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
return result;
}
//乘
public int mut(int n1,int n2){
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
return result;
}
//除
public int div(int n1,int n2){
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
return result;
}
}
3、代理类
package cn.liuw.aop;
import java.util.Random;
/**
* 静态代理类
*/
public class MathProxy implements IMath {
//被代理的对象
IMath math=new Math();
//加
public int add(int n1, int n2) {
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=math.add(n1, n2);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//减法
public int sub(int n1, int n2) {
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=math.sub(n1, n2);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//乘
public int mut(int n1, int n2) {
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=math.mut(n1, n2);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//除
public int div(int n1, int n2) {
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=math.div(n1, n2);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//模拟延时
public void lazy()
{
try {
int n=(int)new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4、测试运行
package cn.liuw.aop;
public class Test {
IMath math=new MathProxy();
@org.junit.Test
public void test01()
{
int n1=100,n2=5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
}
5、运行结果
6、小结
通过静态代理,是否完全解决了上述的4个问题:
已解决:
1、解决了“开闭原则(OCP)”的问题,因为并没有修改Math类,而扩展出了MathProxy类。
2、解决了“依赖倒转(DIP)”的问题,通过引入接口。
3、解决了“单一职责(SRP)”的问题,Math类不再需要去计算耗时与延时操作,但从某些方面讲MathProxy还是存在该问题。
未解决:
4、如果项目中有多个类,则需要编写多个代理类,工作量大,不好修改,不好维护,不能应对变化。
如果要解决上面的问题,可以使用动态代理。
三、动态代理,使用JDK内置的Proxy实现
只需要一个代理类,而不是针对每个类编写代理类。
在上一个示例中修改代理类MathProxy如下:
package cn.liuw.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;
/**
* 动态代理类
*/
public class DynamicProxy implements InvocationHandler {
//被代理的对象
Object targetObject;
/**
* 获得被代理后的对象
* @param object 被代理的对象
* @return 代理后的对象
*/
public Object getProxyObject(Object object){
this.targetObject=object;
return Proxy.newProxyInstance(
targetObject.getClass().getClassLoader(), //类加载器
targetObject.getClass().getInterfaces(), //获得被代理对象的所有接口
this); //InvocationHandler对象
//loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来生成代理对象进行加载
//interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
//h:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上,间接通过invoke来执行
}
/**
* 当用户调用对象中的每个方法时都通过下面的方法执行,方法必须在接口
* proxy 被代理后的对象
* method 将要被执行的方法信息(反射)
* args 执行方法时需要的参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//被织入的内容,开始时间
long start=System.currentTimeMillis();
lazy();
//使用反射在目标对象上调用方法并传入参数
Object result=method.invoke(targetObject, args);
//被织入的内容,结束时间
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//模拟延时
public void lazy()
{
try {
int n=(int)new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试运行:
package cn.liuw.aop;
public class Test {
//实例化一个MathProxy代理对象
//通过getProxyObject方法获得被代理后的对象
IMath math=(IMath)new DynamicProxy().getProxyObject(new Math());
@org.junit.Test
public void test01()
{
int n1=100,n2=5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
IMessage message=(IMessage) new DynamicProxy().getProxyObject(new Message());
@org.junit.Test
public void test02()
{
message.message();
}
}
小结:
JDK内置的Proxy动态代理可以在运行时动态生成字节码,而没必要针对每个类编写代理类。中间主要使用到了一个接口InvocationHandler与Proxy.newProxyInstance静态方法,参数说明如下:使用内置的Proxy实现动态代理有一个问题:被代理的类必须实现接口,未实现接口则没办法完成动态代理。如果项目中有些类没有实现接口,则不应该为了实现动态代理而刻意去抽出一些没有实例意义的接口,通过cglib可以解决该问题。
四、动态代理,使用cglib实现
CGLIB(Code Generation Library)是一个开源项目,是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口,通俗说cglib可以在运行时动态生成字节码。
4.1、引用cglib,通过maven
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.3</version>
</dependency>
4.2、使用cglib完成动态代理
大概的原理是:cglib继承被代理的类,重写方法,织入通知,动态生成字节码并运行,因为是继承所以final类是没有办法动态代理的。具体实现如下:
package cn.liuw.aop;
import java.lang.reflect.Method;
import java.util.Random;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/*
* 动态代理类
* 实现了一个方法拦截器接口
*/
public class DynamicProxy implements MethodInterceptor {
// 被代理对象
Object targetObject;
//Generate a new class if necessary and uses the specified callbacks (if any) to create a new object instance.
//Uses the no-arg constructor of the superclass.
//动态生成一个新的类,使用父类的无参构造方法创建一个指定了特定回调的代理实例
public Object getProxyObject(Object object) {
this.targetObject = object;
//增强器,动态代码生成器
Enhancer enhancer=new Enhancer();
//回调方法
enhancer.setCallback(this);
//设置生成类的父类类型
enhancer.setSuperclass(targetObject.getClass());
//动态生成字节码并返回代理对象
return enhancer.create();
}
// 拦截方法
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 被织入的横切内容,开始时间 before
long start = System.currentTimeMillis();
lazy();
// 调用方法
Object result = methodProxy.invoke(targetObject, args);
// 被织入的横切内容,结束时间
Long span = System.currentTimeMillis() - start;
System.out.println("共用时:" + span);
return result;
}
// 模拟延时
public void lazy() {
try {
int n = (int) new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
参数:Object为由CGLib动态生成的代理类实例,Method为上文中实体类所调用的被代理的方法引用,Object[]为参数值列表,MethodProxy为生成的代理类对方法的代理引用。
返回:从代理实例的方法调用返回的值。
测试运行:
package cn.liuw.aop;
public class Test {
//实例化一个DynamicProxy代理对象
//通过getProxyObject方法获得被代理后的对象
Math math=(Math)new DynamicProxy().getProxyObject(new Math());
@org.junit.Test
public void test01()
{
int n1=100,n2=5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
//另一个被代理的对象,不再需要重新编辑代理代码
Message message=(Message) new DynamicProxy().getProxyObject(new Message());
@org.junit.Test
public void test02()
{
message.message();
}
}
4.3、小结
使用cglib可以实现动态代理,即使被代理的类没有实现接口,但被代理的类必须不是final类。
五、使用Spring实现AOP
横切关注点:跨越应用程序多个模块的方法或功能。(软件系统,可以看做由一组关注点即业务或功能或方法组成。其中,直接的业务关注点是直切关注点,而为直切关注点服务的,就是横切关注点。)即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。
切面(ASPECT):横切关注点被模块化的特殊对象。即,它是一个类。
通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。
目标(Target):被通知对象。
代理(Proxy):向目标对象应用通知之后创建的对象。
切入点(PointCut):切面通知执行的“地点”的定义。
连接点(JointPoint):与切入点匹配的执行点。
下面示意图:
SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:
5.1、定义通知(Advice)
前置通知
package cn.liuw.aspect;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
/**
* @ClassName BeforeAdvice
* @Description 前置通知
**/
public class BeforeAdvice implements MethodBeforeAdvice{
/**
* @param method:方法
* @param objects:参数
* @param target:被代理的目标对象
* @throws Throwable
*/
@Override
public void before(Method method, Object[] objects, Object target) throws Throwable {
System.out.println("-----前置通知-----");
}
}
后置通知
package cn.liuw.aspect;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
/**
* @ClassName AfterAdvice
* @Description 前置通知
**/
public class AfterAdvice implements AfterReturningAdvice{
/**
* @param returnValue:返回值
* @param method:被调用的方法
* @param objects:方法参数
* @param target:被代理对象
* @throws Throwable
*/
@Override
public void afterReturning(Object returnValue, Method method, Object[] objects, Object target) throws Throwable {
System.out.println("-----后置通知-----");
}
}
环绕通知
package cn.liuw.aspect;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
/**
* @ClassName AfterAdvice
* @Description 前置通知
**/
public class SurroundAdvice implements MethodInterceptor{
@Override
public Object invoke(MethodInvocation method) throws Throwable {
//前置横切逻辑
System.out.println("方法"+method.getMethod()+"被调用在对象"+method.getThis()+"上,参数"+method.getArguments());
//方法调用
Object result = method.proceed();
//后置横切逻辑
System.out.println("返回值:"+result);
return result;
}
}
5.2、创建代理工厂、设置被代理对象、添加通知
import cn.liuw.aspect.AfterAdvice;
import cn.liuw.aspect.BeforeAdvice;
import cn.liuw.aspect.SurroundAdvice;
import org.junit.Test;
import org.springframework.aop.framework.ProxyFactory;
import cn.liuw.aspect.Math;
public class Test05 {
@Test
public void test01(){
//实例化Spring代理工厂
ProxyFactory factory = new ProxyFactory();
//设置被代理的对象
factory.setTarget(new Math());
//添加通知,横切逻辑
factory.addAdvice(new BeforeAdvice());
factory.addAdvice(new AfterAdvice());
factory.addAdvice(new SurroundAdvice());
//从代理工厂中获得代理对象
Math math = (Math) factory.getProxy();
int n1 = 100,n2 = 5;
math.add(n1,n2);
math.sub(n1,n2);
math.mut(n1,n2);
math.div(n1,n2);
}
}
运行结果:
-----前置通知-----
方法public int cn.liuw.aspect.Math.add(int,int)被调用在对象cn.liuw.aspect.Math@31a75098上,参数[Ljava.lang.Object;@5f9c449f
100+5=105
返回值:105
-----后置通知-----
-----前置通知-----
方法public int cn.liuw.aspect.Math.sub(int,int)被调用在对象cn.liuw.aspect.Math@31a75098上,参数[Ljava.lang.Object;@2af590c6
100-5=95
返回值:95
-----后置通知-----
-----前置通知-----
方法public int cn.liuw.aspect.Math.mut(int,int)被调用在对象cn.liuw.aspect.Math@31a75098上,参数[Ljava.lang.Object;@5ff3cd32
100X5=500
返回值:500
-----后置通知-----
-----前置通知-----
方法public int cn.liuw.aspect.Math.div(int,int)被调用在对象cn.liuw.aspect.Math@31a75098上,参数[Ljava.lang.Object;@7f24bed4
100/5=20
返回值:20
-----后置通知-----
Process finished with exit code 0
5.3、封装代理创建逻辑
在上面的示例中如果要代理不同的对象需要反复创建ProxyFactory对象,代码会冗余。同样以实现方法耗时为示例代码如下:
1、创建一个环绕通知:
package cn.liuw.aspect;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.util.Random;
/**
* 用于完成计算方法执行时长的环绕通知
**/
public class TimeSpanAdvice implements MethodInterceptor{
@Override
public Object invoke(MethodInvocation method) throws Throwable {
//被织入的横切内容,开始时间before
long start = System.currentTimeMillis();
//延迟
lazy();
//方法调用
Object result = method.proceed();
//被织入的横切内容,结束时间
long span = System.currentTimeMillis() - start;
System.out.println("总共用时:"+span);
return result;
}
//延迟
public void lazy(){
try {
int n = new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2、封装动态代理类
package cn.liuw.aspect;
import org.springframework.aop.framework.ProxyFactory;
/**
* 动态代理类
**/
public class DynamicProxy {
public static Object getProxy(Object obj){
//实例化Spring代理工厂
ProxyFactory factory = new ProxyFactory();
//设置被代理对象
factory.setTarget(obj);
//添加通知,横切逻辑
factory.addAdvice(new TimeSpanAdvice());
return factory.getProxy();
}
}
3、测试运行
import cn.liuw.aspect.*;
import cn.liuw.aspect.Math;
import org.junit.Test;
import org.springframework.aop.framework.ProxyFactory;
public class Test05 {
@Test
public void test02(){
//实例化Spring代理工厂
Math math = (Math) DynamicProxy.getProxy(new Math());
int n1 = 100,n2 = 5;
math.add(n1,n2);
math.sub(n1,n2);
math.mut(n1,n2);
math.div(n1,n2);
}
}
4、运行结果
100+5=105
总共用时:165
100-5=95
总共用时:86
100X5=500
总共用时:49
100/5=20
总共用时:185
Process finished with exit code 0
封装动态代理类:
package cn.liuw.aop;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
public class SpringProxy<T> implements MethodInterceptor {
/**获得代理后的对象*/
public T getProxyObject(Object target){
//代理工厂
ProxyFactory proxy=new ProxyFactory();
//添加被代理的对象
proxy.setTarget(target);
//添加环绕通知
proxy.addAdvice(this);
//获得代理后的对象
return (T) proxy.getProxy();
}
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
before();
//调用方法获得结果
Object result=methodInvocation.proceed();
after(result);
return result;
}
public void before(){
System.out.println("调用方法前");
}
public void after(Object result){
System.out.println("调用方法后"+result);
}
}
六、使用XML配置Spring AOP切面
6.1、添加引用
需要引用一个新的jar包:aspectjweaver,该包是AspectJ的组成部分。可以去http://search.maven.org搜索后下载或直接在maven项目中添加依赖。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
6.2、定义通知
该通知不再需要实现任何接口或继承抽象类,一个普通的bean即可,方法可以带一个JoinPoint连接点参数,用于获得连接点信息,如方法名,参数,代理对象等。
package cn.liuw.aspect;
import org.aspectj.lang.JoinPoint;
/**
* @ClassName Advices
* @Description 通知
**/
public class Advices {
//前置通知
public void before(JoinPoint joinPoint){
System.out.println("-----before-----");
System.out.println("方法名:"+joinPoint+" ,参数:"+joinPoint.getArgs().length+" ,代理对象:"+joinPoint.getTarget());
}
//后置通知
public void after(JoinPoint joinPoint){
System.out.println("-----after-----");
}
}
通知的类型有多种,有些参数会不一样,特别是环绕通知,通知类型如下:
//前置通知
public void beforeMethod(JoinPoint joinPoint)
//后置通知
public void afterMethod(JoinPoint joinPoint)
//返回值通知
public void afterReturning(JoinPoint joinPoint, Object result)
//抛出异常通知
//在方法出现异常时会执行的代码可以访问到异常对象,可以指定在出现特定异常时在执行通知代码
public void afterThrowing(JoinPoint joinPoint, Exception ex)
//环绕通知
//环绕通知需要携带ProceedingJoinPoint类型的参数
//环绕通知类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法。
//而且环绕通知必须有返回值,返回值即为目标方法的返回值
public Object aroundMethod(ProceedingJoinPoint pjd)
6.3、配置IOC容器依赖的XML文件applicationContext.xml
<?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:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="advice" class="cn.liuw.aspect.Advices" />
<bean id="math" class="cn.liuw.aspect.Math" />
<!-- AOP -->
<aop:config proxy-target-class="true">
<!-- 切面配置 -->
<!-- ref表示通知对象的引用 -->
<aop:aspect ref="advice">
<!-- 配置切入点 -->
<aop:pointcut id="pointcut" expression="execution(* cn.liuw.aspect.Math.*(..))" />
<!-- 声明通知,method指定通知类型,pointcut指定切点,就是该通知应该注入哪些方法中 -->
<aop:before method="before" pointcut-ref="pointcut" />
<aop:after method="after" pointcut-ref="pointcut" />
</aop:aspect>
</aop:config>
</beans>
- <bean id="advice" class="com.zhangguo.Spring041.aop08.Advices" />表示通知bean,也就是横切逻辑bean。
- <aop:config proxy-target-class="true">用于AOP配置,proxy-target-class属性表示被代理的类是否为一个没有实现接口的类,Spring会依据实现了接口则使用JDK内置的动态代理,如果未实现接口则使用cblib;在Bean配置文件中,所有的Spring AOP配置都必须定义在<aop:config>元素内部。
- 对于每个切面而言,都要创建一个<aop:aspect>元素来为具体的切面实现引用后端Bean实例。因此,切面Bean必须有一个标识符,供<aop:aspect>元素引用。
- aop:aspect表示切面配置, ref表示通知对象的引用;aop:pointcut是配置切入点,就是横切逻辑将注入的精确位置,那些包,类,方法需要拦截注入横切逻辑。
- aop:before用于声明通知,method指定通知类型,pointcut指定切点,就是该通知应该注入那些方法中。在aop Schema中,每种通知类型都对应一个特定地XML元素。通知元素需要pointcut-ref属性来引用切入点,或者用pointcut属性直接嵌入切入点表达式。
method属性指定切面类中通知方法的名称。有如下几种:
<!-- 前置通知 -->
<aop:before method="before" pointcut-ref="pointcut"/>
<!-- 后置通知 -->
<aop:after method="after" pointcut-ref="pointcut"/>
<!--环绕通知 -->
<aop:around method="around" pointcut="execution(* cn.liuw.aspect.Math.s*(..))"/>
<!--异常通知 -->
<aop:after-throwing method="afterThrowing" pointcut="execution(* cn.liuw.aspect.Math.d*(..))" throwing="exp"/>
<!-- 返回值通知 -->
<aop:after-returning method="afterReturning" pointcut="execution(* cn.liuw.aspect.Math.m*(..))" returning="result"/>
6.3.1 表达式类型
标准的Aspectj Aop的pointcut的表达式类型是很丰富的,但是Spring Aop只支持其中的9种,外加Spring Aop自己扩充的一种一共是10种类型的表达式,分别如下。
- execution:一般用于指定方法的执行,用的最多。
- within:指定某些类型的全部方法执行,也可用来指定一个包。
- this:Spring Aop是基于代理的,生成的bean也是一个代理对象,this就是这个代理对象,当这个对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
- target:当被代理的对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
- args:当执行的方法的参数是指定类型时生效。
- @target:当代理的目标对象上拥有指定的注解时生效。
- @args:当执行的方法参数类型上拥有指定的注解时生效。
- @within:与@target类似,看官方文档和网上的说法都是@within只需要目标对象的类或者父类上有指定的注解,则@within会生效,而@target则是必须是目标对象的类上有指定的注解。而根据笔者的测试这两者都是只要目标类或父类上有指定的注解即可。
- @annotation:当执行的方法上拥有指定的注解时生效。
- bean:当调用的方法是指定的bean的方法时生效。
6.3.2 使用示例
(1) execution
execution是使用的最多的一种Pointcut表达式,表示某个方法的执行,其标准语法如下。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
modifiers-pattern表示方法的访问类型,public等;
ret-type-pattern表示方法的返回值类型,如String表示返回类型是String,“*”表示所有的返回类型;
declaring-type-pattern表示方法的声明类,如“com.elim..*”表示com.elim包及其子包下面的所有类型;
name-pattern表示方法的名称,如“add*”表示所有以add开头的方法名;
param-pattern表示方法参数的类型,name-pattern(param-pattern)其实是一起的表示的方法集对应的参数类型,如“add()”表示不带参数的add方法,“add(*)”表示带一个任意类型的参数的add方法,“add(*,String)”则表示带两个参数,且第二个参数是String类型的add方法;
throws-pattern表示异常类型;其中以问号结束的部分都是可以省略的。
- 1、“execution(* add())”匹配所有的不带参数的add()方法。
- 2、“execution(public * com..*.add*(..))”匹配所有com包及其子包下所有类的以add开头的所有public方法。
- 3、“execution(* *(..) throws Exception)”匹配所有抛出Exception的方法。
(2) within
within是用来指定类型的,指定类型中的所有方法将被拦截。
- 1、“within(com.spring.aop.service.UserServiceImpl)”匹配UserServiceImpl类对应对象的所有方法外部调用,而且这个对象只能是UserServiceImpl类型,不能是其子类型。
- 2、“within(com.elim..*)”匹配com.elim包及其子包下面所有的类的所有方法的外部调用。
(3) this
Spring Aop是基于代理的,this就表示代理对象。this类型的Pointcut表达式的语法是this(type),当生成的代理对象可以转换为type指定的类型时则表示匹配。基于JDK接口的代理和基于CGLIB的代理生成的代理对象是不一样的。
- 1、“this(com.spring.aop.service.IUserService)”匹配生成的代理对象是IUserService类型的所有方法的外部调用。
(4) target
Spring Aop是基于代理的,target则表示被代理的目标对象。当被代理的目标对象可以被转换为指定的类型时则表示匹配。
- 1、“target(com.spring.aop.service.IUserService)”则匹配所有被代理的目标对象能够转换为IUserService类型的所有方法的外部调用。
(5) args
args用来匹配方法参数的。
- 1、“args()”匹配任何不带参数的方法。
- 2、“args(java.lang.String)”匹配任何只带一个参数,而且这个参数的类型是String的方法。
- 3、“args(..)”带任意参数的方法。
- 4、“args(java.lang.String,..)”匹配带任意个参数,但是第一个参数的类型是String的方法。
- 5、“args(..,java.lang.String)”匹配带任意个参数,但是最后一个参数的类型是String的方法。
(6) @target
@target匹配当被代理的目标对象对应的类型及其父类型上拥有指定的注解时。
- 1、“@target(com.spring.support.MyAnnotation)”匹配被代理的目标对象对应的类型上拥有MyAnnotation注解时。
(7) @args
@args匹配被调用的方法上含有参数,且对应的参数类型上拥有指定的注解的情况。
- 1、“@args(com.spring.support.MyAnnotation)”匹配方法参数类型上拥有MyAnnotation注解的方法调用。如我们有一个方法add(MyParam param)接收一个MyParam类型的参数,而MyParam这个类是拥有注解MyAnnotation的,则它可以被Pointcut表达式“@args(com.elim.spring.support.MyAnnotation)”匹配上。
(8) @within
@within用于匹配被代理的目标对象对应的类型或其父类型拥有指定的注解的情况,但只有在调用拥有指定注解的类上的方法时才匹配。
- 1、“@within(com.spring.support.MyAnnotation)”匹配被调用的方法声明的类上拥有MyAnnotation注解的情况。比如有一个ClassA上使用了注解MyAnnotation标注,并且定义了一个方法a(),那么在调用ClassA.a()方法时将匹配该Pointcut;如果有一个ClassB上没有MyAnnotation注解,但是它继承自ClassA,同时它上面定义了一个方法b(),那么在调用ClassB().b()方法时不会匹配该Pointcut,但是在调用ClassB().a()时将匹配该方法调用,因为a()是定义在父类型ClassA上的,且ClassA上使用了MyAnnotation注解。但是如果子类ClassB覆写了父类ClassA的a()方法,则调用ClassB.a()方法时也不匹配该Pointcut。
(9) @annotation
@annotation用于匹配方法上拥有指定注解的情况。
- 1、“@annotation(com.spring.support.MyAnnotation)”匹配所有的方法上拥有MyAnnotation注解的方法外部调用。
(10) bean
bean用于匹配当调用的是指定的Spring的某个bean的方法时。
- 1、“bean(abc)”匹配Spring Bean容器中id或name为abc的bean的方法调用。
- 2、“bean(user*)”匹配所有id或name为以user开头的bean的方法调用。
6.3.4 表达式组合
表达式的组合其实就是对应的表达式的逻辑运算,与、或、非。可以通过它们把多个表达式组合在一起。
- 1、“bean(userService) && args()”匹配id或name为userService的bean的所有无参方法。
- 2、“bean(userService) || @annotation(MyAnnotation)”匹配id或name为userService的bean的方法调用,或者是方法上使用了MyAnnotation注解的方法调用。
- 3、“bean(userService) && !args()”匹配id或name为userService的bean的所有有参方法调用。
七、 基于Aspectj注解的Pointcut表达式应用
在使用基于Aspectj注解的Spring Aop时,我们可以把通过@Pointcut注解定义Pointcut,指定其表达式,然后在需要使用Pointcut表达式的时候直接指定Pointcut。
1、AOP注解方式使用
配置applicationContext.xml文件:
<?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:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 自动扫描所有注解 -->
<context:component-scan base-package="cn.liuw.*" />
<!-- 开启aop注解自动扫描 -->
<aop:aspectj-autoproxy />
</beans>
使用注解定义切面类:
package cn.liuw.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@Pointcut("execution(* cn.liuw.aspect.Math.add(..))")
private void beforeAdd(){}
@Before("beforeAdd()")
public void before(){
System.out.println("--- before ---");
}
}
测试类:
@Test
public void test03(){
//容器
ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
//从代理工厂中获得代理对象
Math math=(Math)ctx.getBean("math");
int n1 = 100,n2 = 5;
math.add(n1,n2);
math.sub(n1,n2);
math.mut(n1,n2);
math.div(n1,n2);
}
运行结果:
--- before ---
100+5=105
100-5=95
100X5=500
100/5=20
2、零配置实现Spring IoC与AOP
package cn.liuw.aop;
import cn.liuw.po.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
//用于表示当前类为容器的配置类,类似<beans/>
@Configuration
//扫描的范围,相当于xml配置的结点<context:component-scan/>
@ComponentScan(basePackages="cn.liuw")
//自动代理,相当于<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class ApplicationCfg {
//在配置中声明一个bean,相当于<bean id="getUser" class="cn.liuw.po.User"/>
@Bean
public User getUser(){
return new User();
}
}