aop技术使用了23中设计模式中的代理模式,UML图如下
,代理对象除可实现与目标对象完全相同的行为外,还可以在目标方法执行前后进行额外的操作,比如记录日志
java中分为静态代理与动态代理两种模式,前者使用专门的编译器来生成遵守java字节码编码规范的Class文件,代表框架AspectJ,它定义了Aop语法,这种方式也称作编译时增强;后者指在运行时在内存中生成动态代理类,也被称为运行时增强。
Spring aop与Aspectj的关系,前者动态代理最终是用的JDK api或者CGLIB来实现的;只是spring支持了Aspect的注解标签,没有依赖原生的aspect编译器;
Aop技术实现主要有Spring AOP与AspectJ两种方式,spring 实现了Aop技术的20%,支撑了80%的需求
AOP术语:
1) Aspect :切面,切入系统的一个切面。比如事务管理是一个切面,权限管理也是一个切面;
2) Join point :连接点,也就是可以进行横向切入的位置;
3) Advice :通知,切面在某个连接点执行的操作(分为五大通知: Before advice , After returning advice , After throwing advice , After (finally) advice , Around advice );
4) Pointcut :切点,符合切点表达式的连接点,也就是真正被切入的地方;
项目中spring作为全能框架被用的最多,而spring aop主要用的动态代理,因此我们先来介绍动态代理,先看其实现方式
动态代理主要有两种方式,一种是通过jdk原生方式实现,该种方式要求代理类只能代理接口,生成的代理类要利用接口承接,若使用目标对象本身则会出错,如下,将变量p改为AopTest修饰则会出现ClassCastException异常
public class AopTest implements InvocationHandler , TargetOperate{
AopTest op;
public AopTest() {}
public AopTest(AopTest op) {
this.op = op;
}
public static void main(String[] args) {
AopTest t= new AopTest();
TargetOperate p = (TargetOperate) (Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
t.getClass().getInterfaces(),
new AopTest(t)));
p.execute("ett");
}
/**
* proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0
* method:我们所要调用某个对象真实的方法的Method对象
* args:指代代理对象方法传递的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before execute");
op.execute(args[0].toString());
System.out.println("after execute");
return null;
}
@Override
public void execute(String name) {
System.out.println(name);
}
}
另一种实现aop的方式则是使用cglib动态代理,该种方式不需要实现特定接口,避免了jdk aop方式的侵入性
t java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibTest implements MethodInterceptor {
CglibTest target;
public void execute() {
System.out.println("execute");
}
public static void main(String[] args) {
CglibTest test = new CglibTest();
test.target = test;
// 生成代理类
Enhancer enhancer = new Enhancer();
// 设置要代理的目标类
enhancer.setSuperclass(CglibTest.class);
// 设置要代理的拦截器
enhancer.setCallback(test);
CglibTest proxy = (CglibTest)enhancer.create();
proxy.execute();;
}
/**
* 拦截方法
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("before execute");
this.target.execute();
System.out.println("after execute");
return null;
}
}
上面无论是jdk还是cglib的代理都是直接拦截目标对象的所有方法,且只有一个拦截器,我们一般不用拦截所有的,spring对此进行的处理方法是使用多种拦截器,来实现五大通知的特性。spring aop使用方式有配置文件与注解两种使用方式,我们先看配置文件方式(Spring与jdk或者cglib的底层交互在此不做详解,有意者可去看下源码)
<?xml version="1.0" encoding="UTF-8"?>
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd ">
<bean name="proxy" class="com.test.SpringProxy"></bean>
<bean name="target" class="com.test.SpringTarget"></bean>
<aop:config>
<aop:pointcut expression="execution(* com.test.SpringTarget.*(..))" id="pc"/>
<aop:aspect ref="proxy">
<aop:before method="before" pointcut-ref="pc"/>
<aop:after method="after" pointcut-ref="pc"/>
<aop:around method="around" pointcut-ref="pc"/>
<aop:after-returning method="afterReturning" pointcut-ref="pc" returning="val"/>
<aop:after-throwing method="afterThrowing" throwing="e" pointcut-ref="pc"/>
</aop:aspect>
</aop:config>
</beans>
返回值与异常通知时可指定参数进入拦截的方法里面,但参数名要保持一致,否则会抛出异常
package com.test;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
public class SpringProxy {
public void before() {
System.out.println("before");
}
public void after() {
System.out.println("after");
}
/**
*JoinPoint
java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
Signature getSignature() :获取连接点的方法签名对象;
java.lang.Object getTarget() :获取连接点所在的目标对象;
java.lang.Object getThis() :获取代理对象本身;
2)ProceedingJoinPoint 只支持around 通知,其他通知会报错,只能用JointPoint
ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法:
java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法;
Java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。
* @param pjp
* @return
* @throws Throwable
*/
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around");
Object ar = pjp.proceed();
System.out.println("around");
return ar;
}
public void afterException(Exception pjp) throws Throwable {
System.out.println("出事了,出现异常了" );
}
// 后置通知
public Object afterReturning(JoinPoint point, Object val){
System.out.println("这是后置通知(如果出现异常不会调用!!)" + val);
return null;
}
public void afterThrowing (Exception e) {
System.out.println("抛出的异常为"+ e);
}
}
package com.test;
public class SpringTarget {
String execute(String name) {
System.out.println("execute:" + name);
if(name.equals("test")) {
throw new RuntimeException("test");
}
return name;
}
}
public class SpringTest {
public static void main(String[] args) {
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml");
SpringTarget t = (SpringTarget)ctx.getBean("target");
String n = t.execute("test1");
System.out.println("end " + n);
}
}
使用xml配置当对象很多的话会很繁琐,于是spring后续提供的注解方式很好的解决了这个问题,aop只需要在配置文件中开启注解扫描,加上这一行
<?xml version="1.0" encoding="UTF-8"?>
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd ">
<bean name="proxy" class="com.test.SpringProxy"></bean>
<bean name="target" class="com.test.SpringTarget"></bean>
<aop:aspectj-autoproxy/>
</beans>
java bean中加上注解@Aspect即可,此时效果与xml一个个配置拦截器效果相同,当一个目标对象有多个代理时执行顺序就不好控制了,此时我们可以使用注解@Order,value的值越小代表优先级越高
@Aspect
public class SpringProxy {
@Pointcut("execution(* com.test.SpringTarget.*(..))")
public void declareJoinPointExpression(){}
@Before("declareJoinPointExpression()")
public void before() {
System.out.println("before");
}
public void after() {
System.out.println("after");
}
而到了springboot时代连配置文件也不用明文开启注解扫描了,直接引入aop-start,我们只需要写好代理类就好了,代理绑定工作spring会帮我们代劳一切