Spring AOP 实现

Stella981
• 阅读 603

AOP(Aspect Orient Programming),我们一般称为面向切面编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务、日志、缓存、分布式锁等等。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。Spring的主要动态代理有CGLib和JDK自动代理。

使用AspectJ的编译时增强实现AOP

AspectJ是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。

编译成字节码.class比原来的.java会多了一些代码,这就是AspectJ的静态代理,它会在编译阶段将Aspect织入Java字节码中, 运行的时候就是经过增强之后的AOP对象。

使用Spring AOP

与AspectJ的静态代理不同,Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

现在我们做一个测试:

首先定义一个接口:

package cn.chinotan.service;

/**
 * @program: test
 * @description: 动物
 * @author: xingcheng
 **/
public interface Animal {

    /**
     * 跑
     * @param where 在什么地方跑
     * @return
     */
    String run (String where);
    
}

其实现类:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }
}

其中@Action为自定义的注解,用来指定aop代理的切入点

package cn.chinotan.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @program: test
 * @description: 动作
 * @author: xingcheng
 **/
@Target(ElementType.METHOD)
public @interface Action {
    
}

定义Aspect:

package cn.chinotan.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @program: test
 * @description: 动作aop实现
 * @author: xingcheng
 **/
@Aspect
@Component
public class ActionAspect {

    @Pointcut("@annotation(cn.chinotan.aop.Action)")
    void actionPointCut() {
    }

    @Before("actionPointCut()")
    void beforeAction() {
        System.out.println("热身运动");
    }
}

其中有几种aop的通知注解:

  1. @Before: 前置通知, 在方法执行之前执行
  2. @After: 后置通知, 在方法执行之后执行
  3. @AfterRunning:返回通知, 在方法成功执行返回结果之后执行
  4. @AfterThrowing: 异常通知, 在方法抛出异常之后
  5. @Around: 环绕通知,围绕着方法执行

@Pointcut是切入点的注解:

这里使用了@annotation 可以在使用了自定义注解的配置方法上实现切入

也可以使用execution(* *(..))的形式:

    声明切入点
    第一个*表示 方法  返回值(例如public int)
    第二个* 表示方法的全限定名(即包名+类名)
    perform表示目标方法参数括号两个.表示任意类型参数
    方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,
    我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么
    execution表示执行的时候触发

在启动的application.yml配置文件中加入

spring.aop.proxy-target-class: false

这个是控制aop的具体实现方式,为true 的话使用cglib,为false的话使用java的Proxy,默认是false

之后运行controller:

package cn.chinotan.controller;

import cn.chinotan.service.Animal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: test
 * @description: test类
 * @author: xingcheng
 **/
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    Animal animal;

    @GetMapping("/aopRun")
    public String aopRun() {
        animal.run("狗窝");
        System.out.println("dog代理为:" + animal.getClass());
        
        return "ok";
    }
    
}

打印日志:

Spring AOP 实现

可以看到类型是com.sun.proxy.$Proxy71,也就是前面提到的Proxy类,因此这里Spring AOP使用了JDK的动态代理。

再来看看不实现接口的情况,修改Dog类:

配置proxy-target-class: false依旧  

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 **/
@Service
public class Dog {

    @Action
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }
}

打印日志:

Spring AOP 实现

可以看到类被CGLIB增强了,也就是动态代理。这里的CGLIB代理就是Spring AOP的代理,这个类也就是所谓的AOP代理,AOP代理类在切点动态地织入了增强处理。

可以看到:

    AspectJ在编译时就增强了目标对象,Spring AOP的动态代理则是在每次运行时动态的增强,生成AOP代理对象,区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

    java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,如果目标对象实现了接口,可以强制使用CGLIB实现AOP,如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

误区注意:

在平时开发中,我们通常在Service中定义了一个方法并且切入之后,从Controller里面调用该方法可以实现切入,但是当在同一个Service中实现另一方法并调用改方法时却无法切入

类似于:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }

    @Override
    public void runToEat(String food) {
        run("狗窝");
        System.out.println("狗在吃" + food);
    }
}


package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }

    @Override
    public void runToEat(String food) {
        run("狗窝");
        System.out.println("狗在吃" + food);
    }
}

我们在执行runToEat方法时,调用了自己类中的另一个方法,结果为:

Spring AOP 实现

可以看到run()的切面方法并没有执行,以上结果的出现与Spring AOP的实现原理息息相关,由于Spring AOP采用了动态代理实现AOP,在Spring容器中的bean(也就是目标对象)会被代理对象代替,代理对象里加入了我们需要的增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截,

通过调用代理对象的action方法,在其内部会经过切面增强,然后方法被发射到目标对象,在目标对象上执行原有逻辑,如果在原有逻辑中嵌套调用了work方法,则此时work方法并没有被进行切面增强,因为此时它已经在目标对象内部。而解决方案很好地说明了,将嵌套方法发射到代理对象,这样就完成了切面增强。可以看下源码:

Spring AOP 实现

在代码3处,如果配置了exposeProxy开关,则会将代理对象暴露在当前线程中,以供其它需要的地方使用,通过使用静态的全局ThreadLocal变量就解决了问题。

spring提供了一个这样的类:

Spring AOP 实现

可以看到他可以获取到当前的aop代理,但是在获取之前,得开启exposeProxy开关

@EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true)

这样就可以进行代理了,打印日志为:

Spring AOP 实现

既然这样可以,那是不是直接applicationContext.getBean()也可以呢?实验过后得到的结果是可行,而且配置中的expose-proxy也不用设置成true,那试一下:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {
    
    @Autowired
    ApplicationContext applicationContext;

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }

    @Override
    public void runToEat(String food) {
//        Dog dog = (Dog) AopContext.currentProxy();
        Dog dog = (Dog) applicationContext.getBean("dog");
        dog.run("狗窝");
        System.out.println("狗在吃" + food);
    }
}

打印日志为:

Spring AOP 实现

可见同样可以

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
AOP系列
AOP(AspectOrientProgramming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等等。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以SpringAOP为代表。本文
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这