Spring AOP详解
AOP
AOP的实现一般都是基于 代理模式 ,在JAVA中采用JDK动态代理模式,但是我们都知道,JDK动态代理模式只能代理接口而不能代理类。因此,Spring AOP 同时支持了 CGLIB、ASPECTJ、JDK动态代理。在不同的场景选择不同的代理方法来实现AOP,开发者也无需关心其选择过程.
如果目标对象实现了接口,Spring AOP 将会默认采用 JDK 动态代理来生成 AOP 代理类;
如果目标对象没有实现接口,Spring AOP 将会选择采用 CGLIB 来生成 AOP 代理类;
代理模式
我们知道AOP思想的实现一般都是基于 代理模式 ,所以非常有必要先了解一下静态代理以及JDK动态代理、CGLIB动态代理的实现方式。
代理模式这种设计模式是一种使用代理对象来执行目标对象的方法并在代理对象中增强目标对象方法的一种设计模式。
可以看到还是很简单的,代理类实现了被代理类的接口,同时与被代理类是组合关系。下面看一下代理模式的几种实现.
静态代理
接口
public interface UserDao {
void add();
void delete();
void update();
void query();
}
目标对象(被代理的对象)
public class UserDaoImp implements UserDao {
@Override
public void add() {
System.out.println("目标对象正在执行add方法");
}
@Override
public void delete() {
System.out.println("目标对象正在执行delete方法");
}
@Override
public void update() {
System.out.println("目标对象正在执行update方法");
}
@Override
public void query() {
System.out.println("目标对象正在执行query方法");
}
}
目标的代理对象
public class UserDaoImpProxy implements UserDao{
UserDaoImp userDaoImp =null;
public UserDaoImpProxy(UserDaoImp userDaoImp){
this.userDaoImp=userDaoImp;
}
@Override
public void add() {
System.out.println("add执行前记录日志--");
userDaoImp.add();
}
@Override
public void delete() {
System.out.println("delete执行前记录日志--");
userDaoImp.delete();
}
@Override
public void update() {
System.out.println("update执行前记录日志--");
userDaoImp.update();
}
@Override
public void query() {
System.out.println("query执行前记录日志--");
userDaoImp.query();
}
}
不难看出,静态代理其实就是将增强功能写死了在代理对象中执行的形式,每次要在接口中添加一个新方法,则需要在目标对象中实现这个方法,并且在代理对象中实现相应的代理方法,然而我们可以使用Java的反射技术,实现动态代理。
JDK动态代理
在讲JDK的动态代理方法之前,不妨先想想如果让你来实现一个可以代理任意类的任意方法的代理类,该怎么实现?大概是用反射来获取被代理对象及其方法,再执行该方法,并在执行前后添加一些增强方法,这样似乎有点naive,让我们一起看看JDK的动态代理吧.
首先介绍一下最核心的一个接口和一个方法:
1.java.lang.reflect包里的InvocationHandler接口:
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
我们对于被代理的类的操作都会由该接口中的invoke方法帮我们实现,其中的参数的含义分别是:
- proxy:被代理类的实例
- method:需要被代理的方法
- args:方法需要的参数(即参数method的入参)
我们可以在invoke方法中调用被代理类的方法并获得返回值,自然也可以在调用该方法的前后去做一些额外的事情,从而实现动态代理.
2.另外一个很重要的静态方法是java.lang.reflect包中的Proxy类的newProxyInstance方法:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
该方法会返回一个被修改过(增强)的被代理对象的实例,从而可以自由的调用该实例的方法.其参数如下
loader:被代理的类的类加载器
interfaces:被代理类的接口数组(实现的接口)
h:拦截方法的句柄,即在上文中提到的实现增强功能的接口
直接上实例:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkProxy {
public static void main(String[] args) {
UserDao userDao = new UserDaoImp();//被代理对象
//获得代理对象
UserDao proxyObject =
(UserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader(),
userDao.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("在代理对象中拦截方法:"+method.getName());
System.out.println("执行前--------");
Object o=method.invoke(userDao,args); //调用拦截方法
System.out.println("执行完成---------");
return o;
}
});
//通过代理对象执行方法add
proxyObject.add();
}
}
由于jdk动态代理只能代理实现了某些接口的对象,所以在获取的代理对象的声明类型一定是 接口类型,而不是UserDaoImp实现类.
可以看到对于不同的实现类来说,可以用同一个动态代理类来进行代理,实现了“一次编写到处代理”的效果。但是这种方法有个缺点,就是被代理的类一定要是实现了某个接口的,这很大程度限制了本方法的使用场景。
Cglib动态代理
CGlib是一个字节码增强库,为AOP等提供了底层支持。下面看看它是怎么实现动态代理的。
首先需要导入两个包支持:
import java.lang.reflect.Method;
public class CglibProxy {
public static void main(String[] args) {
UserDao userDao =new UserDaoImp();
Enhancer enhancer=new Enhancer();
//设置代理对象的父类
enhancer.setSuperclass(userDao.getClass());
//设置回掉方法
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
System.out.println("在代理对象中拦截到:"+method.getName());
System.out.println("执行前+++++++");
Object object=method.invoke(userDao,objects);
System.out.println("执行后+++++++");
return object;
}
});
UserDao proxy=(UserDao) enhancer.create();
proxy.delete();
}
}
可以看到,Cglib实现代理的方式是和目标对象使用同一个父类,无论是继承还是实现接口,都是为了代理对象能直接调用目标对象的方法。这种方法可以做到代理任何一个对象的任何方法,不再有jdk动态代理的接口限制.
AOP基本概念
面向切面AOP——Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑。它们并不负责其它的系统级关注点,例如日志或事务支持,但我们可以利用AOP来实现这些系统级关注点。
当你开发一个登陆功能,你可能需要在用户登陆前后进行权限校验并将校验信息(用户名,密码,请求登陆时间,ip地址等)记录在日志文件中,当用户登录进来之后,当他访问某个其他功能时,也需要进行合法性校验.当系统非常地庞大,系统中专门进行权限验证的代码是非常多的,而且非常地散乱,我们就想能不能将这些权限校验、日志记录等非业务逻辑功能的部分独立拆分开,并且在系统运行时需要的地方(连接点)进行动态插入运行,不需要的时候就不使用。
1.基本概念:
通知(Advice),有5种通知类型
- before 在目标方法执行之前被调用
- after 在目标方法完成后调用通知,不管方法是否执行成功
- after-running 在方法成功执行后调用通知
- after-throwing 在方法抛出异常之后调用通知
- around 包围目标方法,在方法调用前后都调用通知
通知并不是某个类或者某个方法, 通知是用来告诉我们(告诉系统何)何时执行切入,规定一个时间,在系统运行中的某个时间点(比如抛异常啦!方法执行前啦!).
2.切点(pointcut)
切点在Spring AOP中可以对应到系统中的某个(某些)方法,指定要切入哪一个方法中.
3.连接点(jointpoint)
在程序执行过程中的任何时间点都可以作为织入点(连接点),如方法调用,方法执行,字段设置,异常处理,类初始化,循环中的某个点等.Spring AOP 目前仅支持方法执行(method execution) .可以理解为, 连接点就是准备在系统中执行切点和切入通知的地方(一般是一个方法或一个字段).
4.切面(Aspect)
Advice 和 Pointcut定义了一个切面Aspect.Advice定义了Aspect的任务和什么时候执行它,而切入点Pointcut定义在哪里具体地方切入,也就是说,Aspect定义了它是什么东西 什么时候切入和在哪里切入。
5.引入(introduction)
允许我们向现有的类添加新的方法或者属性
6.织入(weaving)
Weaving是一个混合横向方面到目标业务对象的过程,织入可以是在编译时间,也可以在运行时间使用classload,Spring AOP缺省是在运行时间。
Spring AOP
Spring的AOP实现内部机制:
jdk动态代理
使用到的类和接口:Proxy,InvocationHandler (只能对接口进行代理)
CGLIB代理(三方库) 可以直接对任意class进行代理
AspectJ需要用到特定的编译器,在编译时期混合代码
引入一个实例:
spring-AOP.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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
<bean id="userAction1" class="springAOP.UserActionImpl"/>
<bean id="userAction2" class="springAOP.UserActionImpl" />
<bean id="aspect" class="springAOP.Aspect"/>
<aop:config>
<aop:aspect ref="aspect" >
<!--定义切点-->
<aop:pointcut id="pointcut" expression="execution(* springAOP.*.*(..))"/>
<!--定义通知-->
<aop:before method="permissionCheck" pointcut-ref="pointcut"/>
<aop:after method="logger" pointcut-ref="pointcut"/>
<aop:after-returning method="saveInfo" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
接口类:
public interface UserAction {
void login(String username);
void download();
}
实现类:
public class UserActionImpl implements UserAction {
@Override
public void login(String username) {
System.out.println(username+"正在登陆中");
}
@Override
public void download() {
System.out.println("正在下载文件中");
}
}
切面:
public class Aspect {
public void logger(){
System.out.println("正在记录日志----------");
}
public void permissionCheck(){
System.out.println("正在校验权限------------");
}
public void saveInfo(){
System.out.println("正在保存用户访问记录=============");
}
}
测试代码:
public class AOPTest {
public static void main(String[] args) {
ApplicationContext context= new ClassPathXmlApplicationContext("spring-AOP.xml");
UserAction action1 =(UserAction)context.getBean("userAction1");
UserAction action2 =(UserAction) context.getBean("userAction2");
action1.login("用户1");
action1.download();
System.out.println("----------------------------------------------------------------");
action2.login("用户2");
action2.download();
}
}
基于注解的AOP实现
首先,不要忘记导入jar包
spring-AOP.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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--扫描包-->
<context:component-scan base-package="annotationAOP"/>
<!--启用自动代理,自动为切面方法中匹配的方法所在的类生成代理对象-->
<aop:aspectj-autoproxy>
</aop:aspectj-autoproxy>
</beans>
接口文件:
public interface Calculator <T extends Number>{
T add(T t1,T t2);
T subtract(T t1, T t2);
T divide(T t1,T t2) ;
T multiply(T ti,T t2);
}
实现类:(不要忘记加@Component注解 ,将其交由spring管理)
@Component("calculatorImp")
public class CalculatorImp implements Calculator{
@Override
public Number add(Number t1, Number t2) {
return t1.doubleValue()+t2.doubleValue();
}
@Override
public Number subtract(Number t1, Number t2) {
return t1.doubleValue()-t2.doubleValue();
}
@Override
public Number divide(Number t1, Number t2) {
if (t2.doubleValue()==0)
throw new ArithmeticException("/ by 0");
return t1.doubleValue()/t2.doubleValue();
}
@Override
public Number multiply(Number t1, Number t2) {
return t1.doubleValue()*t2.doubleValue() ;
}
}
切面类:(这里将annotationAOP包下的所有方法都切入),注意@Component与@Aspect(将其标注为切面类)
@Component
@Aspect
public class LogAspect {
@Before(value = "execution(* annotationAOP.*.*(..))")
public void before(JoinPoint joinPoint){
System.out.println("[前置通知]方法"+joinPoint.getSignature().getName()+"运行前,"+"参数"+ Arrays.asList(joinPoint.getArgs()));
}
@AfterReturning(value = "execution(* annotationAOP.*.*(..))",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result ){
System.out.println("[返回通知]方法"+joinPoint.getSignature().getName()+"正常返回,返回值"+result);
}
@AfterThrowing(value = "execution(* annotationAOP.*.*(..))",throwing = "e")
public void afterThrowing(JoinPoint joinPoint,Exception e){
System.out.println("[异常通知]方法"+joinPoint.getSignature().getName()+"抛出异常"+e.getMessage());
}
@After(value = "execution(* annotationAOP.*.*(..))")
public void after(JoinPoint joinPoint){
System.out.println("[后置通知]方法"+joinPoint.getSignature().getName()+"运行结束");
}
}
测试类:
public class Test {
public static void main(String[] args) {
ApplicationContext context =new ClassPathXmlApplicationContext("spring-AOP.xml");
Calculator calculator=(Calculator) context.getBean("calculatorImp");
Number result1 =calculator.add(1,2);
System.out.println("结果:"+result1);
System.out.println("--------------------------------------------------");
//这个方法会报抛出异常
Number result2=calculator.divide(3,0);
System.out.println("结果:"+result2);
System.out.println("----------------------------------------------------");
}
}
测试结果:可以看到我们的切面方法都织入到了目标方法中
环绕通知:
切面类:(环绕通知)
@Component
@Aspect
public class LogAspect {
@Around(value = "execution(* annotationAOP.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
Object result = null;
try {
System.out.println("[环绕通知-->前置通知]方法" + proceedingJoinPoint.getSignature().getName() + "运行前," + "参数" + Arrays.asList(proceedingJoinPoint.getArgs()));
result = proceedingJoinPoint.proceed();
System.out.println("[环绕通知-->返回通知]方法" + proceedingJoinPoint.getSignature().getName() + "运行结束,返回" + result);
} catch (Throwable e) {
System.out.println("[环绕通知-->异常通知]方法" + proceedingJoinPoint.getSignature().getName() + "抛出异常" + e.getLocalizedMessage());
}
System.out.println("[环绕通知-->后置通知]方法" + proceedingJoinPoint.getSignature().getName() + "运行结束");
return result;
}
}