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;
    }
 
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343