Spring笔记(5)--AOP


一、 AOP动态代理

1、简介

AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

底层原理:实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。

底层实现在Spring中,框架会根据目标类是否实现接口来决定采用哪种动态代理的方式。

  1. 实现接口:JDK动态代理(基于接口的动态代理技术)
  2. 未实现接口:cglib动态代理(基于父类的动态代理技术)

2、JDK动态代理

  1. 目标类接口
    public interface TargetInterface {
     public void method();
    }
  2. 目标类
    public class Target implements TargetInterface {
     @Override
     public void method() {
         System.out.println("Target running....");
     }
    }
  3. 动态代理
    Target target = new Target(); //创建目标对象
    //创建代理对象
    TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(target.getClass()
    .getClassLoader(),target.getClass().getInterfaces(),new InvocationHandler() {
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         System.out.println("前置增强代码...");
         Object invoke = method.invoke(target, args);
         System.out.println("后置增强代码...");
         return invoke;
      }
    }
    );
  4. 调用代理对象的方法测试
    // 测试,当调用接口的任何方法时,代理对象的代码都无序修改
    proxy.method();
  5. 输出
    前置增强代码...
    Target running...
    后置增强代码...

3、cglib的动态代理

  1. 目标类
    public class Target {
     public void method() {
         System.out.println("Target running....");
     }
    }
  2. 动态代理代码
    Target target = new Target(); //创建目标对象
    Enhancer enhancer = new Enhancer();   //创建增强器
    enhancer.setSuperclass(Target.class); //设置父类
    enhancer.setCallback(new MethodInterceptor() { //设置回调
     @Override
     public Object intercept(Object o, Method method, Object[] objects, 
     MethodProxy methodProxy) throws Throwable {
         System.out.println("前置代码增强....");
         Object invoke = method.invoke(target, objects);
         System.out.println("后置代码增强....");
         return invoke;
     }
    });
    Target proxy = (Target) enhancer.create(); //创建代理对象
  3. 调用代理对象的方法测试
    //测试,当调用接口的任何方法时,代理对象的代码都无序修改
    proxy.method();
  4. 输出同上

4、AOP常用术语

  1. Target(目标对象):代理的目标对象
  2. Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类
  3. Joinpoint(连接点):指被拦截的点。在Spring中,这些点指得是方法,因为spring只支持方法类型的连接点。
  4. Pointcut(切入点):指对哪些Joinpoint进行拦截的定义。
  5. Advice(通知/增强):指拦截到Joinpoint之后所要做的事情就是通知。
  6. Aspect(切面):切入点和通知(引介)的结合。
  7. Weaving(织入):指吧增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而Aspect采用编译期织入和类装载期织入。

5、AOP开发需明确的事项

1)需要编写的内容

  • 编写核心业务代码(目标类的目标方法)
  • 编写切面类,切面类中有通知(增强功能方法)
  • 在配置文件中,配置织入关系,即将通知和连接点进行结合。

    2)AOP技术实现的内容

    Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

    3)AOP底层使用哪种代理方式

    在Spring中,框架会根据目标类是否实现接口来决定采用哪种动态代理的方式。
  1. 实现接口:JDK动态代理
  2. 未实现接口:cglib动态代理

二、基于XML的AOP开发

① 导入 AOP 相关坐标

② 创建目标接口和目标类(内部有切点)

③ 创建切面类(内部有增强方法)

④ 将目标类和切面类的对象创建权交给 spring

⑤ 在 applicationContext.xml 中配置织入关系

⑥ 测试代码

1、导入AOP相关依赖

<!--导入spring的context坐标,context依赖aop-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.0.5.RELEASE</version>
</dependency>
<!-- aspectj的织入 -->
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.8.13</version>
</dependency>

2、创建目标接口和目标类

public interface TargetInterface {
    public void method();
}

public class Target implements TargetInterface {
    @Override
    public void method() {
        System.out.println("Target running....");
    }
}

3、创建切面类

public class MyAspect {
    //前置增强方法
    public void before(){
        System.out.println("前置代码增强.....");
    }
}

4、将目标类和切面类的对象创建权交给Spring

<!--配置目标类-->
<bean id="target" class="com.itheima.aop.Target"></bean>
<!--配置切面类-->
<bean id="myAspect" class="com.itheima.aop.MyAspect"></bean>

5、导入AOP命名空间

<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/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
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

6、在 applicationContext.xml 中配置织入关系

配置切点表达式和前置增强的织入关系

<aop:config>
    <!--引用myAspect的Bean为切面对象-->
    <aop:aspect ref="myAspect">
        <!--配置Target的method方法执行时要进行myAspect的before方法前置增强-->
        <aop:before method="before" pointcut="execution(public void com.itheima.aop.Target.method())"></aop:before>
    </aop:aspect>
</aop:config>

7、测试代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {
    @Autowired
    private TargetInterface target;
    @Test
    public void test1(){
        target.method();
    }
}

三、基于注解的AOP开发

1、JoinPoint 对象

JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加 JoinPoint 参数,就可以获取到封装了该方法信息的 JoinPoint 对象

@Aspect
@Component
public class aopAspect {

    @Before("execution(public * com.rewind.controller.OrderController.*(..))")
    public void before(JoinPoint joinPoint){
        System.out.println("前置增强,比如创建连接对象");
    }
}

JoinPoint 对象常用api:

// 获取传入目标方法的参数对象
Object[] getArgs();

// 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Signature getSignature();

// 获取被代理的对象
Object getTarget();

// 获取代理对象
Object getThis();

JoinPoint 对象

public interface JoinPoint {  
    String toString();         //连接点所在位置的相关信息  
    String toShortString();     //连接点所在位置的简短相关信息  
    String toLongString();     //连接点所在位置的全部相关信息  
    Object getThis();         //返回AOP代理对象  
    Object getTarget();       //返回目标对象  
    Object[] getArgs();       //返回被通知方法参数列表  
    Signature getSignature();  //返回当前连接点签名  
    SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置  
    String getKind();        //连接点类型  
    StaticPart getStaticPart(); //返回连接点静态部分  
}

JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等:

public interface StaticPart {  
    Signature getSignature();    //返回当前连接点签名  
    String getKind();          //连接点类型  
    int getId();               //唯一标识  
    String toString();         //连接点所在位置的相关信息  
    String toShortString();     //连接点所在位置的简短相关信息  
    String toLongString();     //连接点所在位置的全部相关信息  
}

2、ProceedingJoinPoint

ProceedingJoinPoint 对象是 JoinPoint 的子接口,该对象只用在 @Around 的切面方法中,

添加了以下两个方法

// 执行目标方法
Object proceed();

// 传入的新的参数去执行目标方法
Object proceed(Object[] var1);

3、编写步骤

(1)依赖

<!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

(2)实现

@Aspect
@Component
public class OrderAspect {

    @Before("execution(public * com.rewind.controller.OrderController.*(..))")
    public void before(){
        System.out.println("前置增强,比如创建连接对象");
    }

    @After("execution(public * com.rewind.controller.OrderController.*(..))")
    public void after(){
        System.out.println("后置增强,比如关闭连接对象");
    }

    @AfterReturning("execution(public * com.rewind.controller.OrderController.*(..))")
    public void afterReturning(){
        System.out.println("方法执行成功通知,比如提交事务");
    }

    @AfterThrowing("execution(public * com.rewind.controller.OrderController.*(..))")
    public void afterThrowing(){
        System.out.println("方法执行异常通知,比如回滚事务");
    }

    // 获取到请求的参数/响应值,并且对参数和响应值做出一定修改并生效
    @Around("execution(public * com.rewind.controller.OrderController.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取方法参数值数组
        Object[] args = joinPoint.getArgs();
        //得到其方法签名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //获取方法参数类型数组
        Class[] paramTypeArray = methodSignature.getParameterTypes();
        if (EntityManager.class.isAssignableFrom(paramTypeArray[paramTypeArray.length - 1])) {
            //如果方法的参数列表最后一个参数是entityManager类型,则给其赋值
            args[args.length - 1] = entityManager;
        }
        log.info("请求参数为{}",args);
        //动态修改其参数
        //执行目标方法
        //注意,如果调用joinPoint.proceed()方法,则修改的参数值不会生效,必须调用joinPoint.proceed(Object[] args)
        Result result = (Result)joinPoint.proceed(args);

        if (result.getData() instanceof User) {
            User user = (User) result.getData();
            user.setUsername(user.getUsername() + " &");    // 设置值
        }
        log.info("响应结果为{}",result);
        //如果这里不返回result,则目标对象实际返回值会被置为null
        return result;
    }
}

(3)可简化(提取切点)

@Pointcut("execution(* com.rewind.controller.TestController.*(..))")
//@Pointcut("execution(* com.rewind.controller..*.*(..))")   //切所有controller
public void pointcut() {
}

@Before("pointcut()")
public void beforeControllerMethod(JoinPoint joinPoint) {
    log.info("前置通知!!!!方法执行前执行");
}

(4)切点表达式

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
execution(public * com.rewind.controller.OrderController.*(..))

execution(<返回类型模式><方法名模式>(<参数模式>))
execution(* com.rewind.controller.TestController.*(..))

其中:返回类型模式、方法名模式、参数模式是必选项

修饰符模式和异常模式可以省略,?表示可省略

四、切点指示符

切点表达式:

切点指示符(方法表达式)

切点指示符是切点定义的关键字,切点表达式以切点指示符开始。开发人员使切点指示符来告诉切点将要匹配什么,有以下9种切点指示符:executionwithinthistargetargs@target@args@within@annotation,下面一一介结这9种切点指示符。

1、execution

execution 是一种使用频率比较高比较主要的一种切点指示符,用来匹配方法签名,方法签名使用全限定名,包括访问修饰符(public/private/protected)、返回类型,包名、类名、方法名、参数,其中返回类型,包名,类名,方法,参数是必须的,如下面代码片段所示:

@Pointcut("execution(public String org.baeldung.dao.FooDao.findById(Long))")

上面的代码片段里的表达式精确地匹配到 FooDao 类里的 findById(Long) 方法,但是这看起来不是很灵活。假设我们要匹配 FooDao 类的所有方法,这些方法可能会有不同的方法名,不同的返回值,不同的参数列表,为了达到这种效果,我们可以使用通配符。如下代码片段所示:

@Pointcut("execution(* org.baeldung.dao.FooDao.*(..))")

第一个通配符匹配所有返回值类型,第二个匹配这个类里的所有方法,()括号表示参数列表,括号里的用两个点号表示匹配任意个参数,包括0个

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
execution(public * com.rewind.controller.OrderController.*(..))

execution(<返回类型模式><方法名模式>(<参数模式>))
execution(* com.rewind.controller.TestController.*(..))

其中:返回类型模式、方法名模式、参数模式是必选项

修饰符模式和异常模式可以省略,?表示可省略

符号 含义
* 匹配任何权限修饰符、任何路径;在参数中表示只有一个任意参数方法
.. 指定包及其子包;在参数中表示任意个数的任意参数类型
+ 指定接口及其子类
&& 且(&&)
|| 或(||)
非(!)

AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式。

在Schema风格下,由于在XML中使用“&&”需要使用转义字符“&&”来代替之,所以很不方便,因此Spring ASP 提供了and、or、not来代替&&、||、!。

方法表达式 描述
public * *(..) 任何公共方法的执行
* cn.javass..IPointcutService.*() cn.javass包及所有子包下IPointcutService接口中的任何无参方法
* cn.javass...(..) cn.javass包及所有子包下任何类的任何方法
* cn.javass..IPointcutService.*(*) cn.javass包及所有子包下IPointcutService接口的任何只有一个参数方法
* (!cn.javass..IPointcutService+).*(..) 非cn.javass包及所有子包下IPointcutService接口及子类型”的任何方法
* cn.javass..IPointcut*.test*(java.util.Date) cn.javass包及所有子包下IPointcut前缀类型的的以test开头的只有一个参数类型为java.util.Date的方法,注意该匹配是根据方法签名的参数类型进行匹配的,而不是根据执行时传入的参数类型决定的,如定义方法:public void test(Object obj);即使执行时传入java.util.Date,也不会匹配的;
* cn.javass..IPointcut*.test*(..) throwsIllegalArgumentException, ArrayIndexOutOfBoundsException cn.javass包及所有子包下IPointcut前缀类型的的任何方法,且抛出IllegalArgumentException和ArrayIndexOutOfBoundsException异常
* (cn.javass..IPointcutService+ && java.io.Serializable+).*(..) 任何实现了cn.javass包及所有子包下IPointcutService接口和java.io.Serializable接口的类型的任何方法
@java.lang.Deprecated * *(..) 任何持有@java.lang.Deprecated注解的方法
`@(java.lang.Deprecated
@java.lang.Deprecated @cn.javass..Secure * *(..) 任何持有@java.lang.Deprecated和@cn.javass..Secure注解的方法
(@cn.javass..Secure *) *(..) 返回值类型含有指定注解的方法
* (@cn.javass..Secure *).*(..) 任何定义方法的类型持有@cn.javass..Secure的方法
* *(@cn.javass..Secure (*) , @cn.javass..Secure (*)) 任何签名带有两个参数的方法,且这个两个参数都被@ Secure标记了,如public void test(@Secure String str1,@Secure String str1);
* *((@ cn.javass..Secure *))或* *(@ cn.javass..Secure *) 任何带有一个参数的方法,且该参数类型持有@ cn.javass..Secure;
* *(java.util.Collection<@cn.javass..Secure *>) 任何带有一个参数(类型为java.util.Collection)的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass..Secure注解;如 public void test(Collection<Model> collection);Model类型上持有@cn.javass..Secure

2、within

用于匹配指定类型内的方法执行

//  cn.java包及子包下的任何方法执行
within(cn.java..*)

// java包或所有子包下IPointcutService类型及子类型的任何方法
within(java..IPointcutService+)

// 持有cn..Secure注解的任何类型的任何方法,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
within(@cn..Secure *)

3、this

使用 this(类型全限定名) 匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口方法也可以匹配;注意this中使用的表达式必须是类型全限定名,不支持通配符;

// 当前AOP对象实现了 IPointcutService接口的任何方法
this(cn.javass.spring.chapter6.service.IPointcutService);

// 当前AOP对象实现了 IIntroductionService接口的任何方法,也可能是引入接口
this(cn.javass.spring.chapter6.service.IIntroductionService);

4、target

使用 target(类型全限定名) 匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是类型全限定名,不支持通配符;

以目标对象作为切点的表达式定义方式 (用于某一个接口or某一类接口),人话:切指定接口的实现类的所有方法

语法:target(包名.接口名)

如:target(com.xx.IA) 所有实现了IA接口的实现类,作为代理的目标对象,会自动增加通知的织入,是实现切面

应用:为某一个具体的接口实现提供的配置。如 登录的时候需要执行的附属逻辑是比较多的。在不同的业务流程中,附属逻辑也不同。如,电商中,可能在登录的时候需要去执行购物车合并

// 当前目标对象(非AOP对象)实现了 IPointcutService接口的任何方法
target(cn.javass.spring.chapter6.service.IPointcutService);

// 当前目标对象(非AOP对象) 实现了IIntroductionService 接口的任何方法,不可能是引入接口
target(cn.javass.spring.chapter6.service.IIntroductionService);

this 和 target

this用来匹配的连接点所属的对象引用是某个特定类型的实例,target用来匹配的连接点所属目标对象必须是指定类型的实例;那么这两个有什么区别呢?原来AspectJ在实现代理时有两种方式:

1、如果当前对象引用的类型没有实现自接口时,spring aop使用生成一个基于CGLIB的代理类实现切面编程

2、如果当前对象引用实现了某个接口时,Spring aop使用JDK的动态代理机制来实现切面编程

  • this 指示符就是用来匹配基于CGLIB的代理类,通俗的来讲就是,如果当前要代理的类对象没有实现某个接口的话,则使用 this;

  • target 指示符用于基于JDK动态代理的代理类,通俗的来讲就是如果当前要代理的目标对象有实现了某个接口的话,则使用 target(参数为对应的接口)

public class FooDao implements BarDao {
    ...
}

比如在上面这段代码示例中,spring aop将使用jdk的动态代理来实现切面编程,在编写匹配这类型的目标对象的连接点表达式时要使用target指示符, 如下所示:

@Pointcut("target(org.baeldung.dao.BarDao)")

如果FooDao类没有实现任何接口,或者在spring aop配置属性:proxyTargetClass设为true时,Spring Aop会使用基于CGLIB的动态字节码技为目标对象生成一个子类将为代理类,这时应该使用this指示器:

@Pointcut("this(org.baeldung.dao.FooDao)")

5、args

使用 args(参数类型列表) 匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;

// 任何一个以接受“传入参数类型为 java.io.Serializable” 开头,且其后可跟任意个任意类型的参数的方法执行,args指定的参数类型是在运行时动态匹配的
args(java.io.Serializable,..);

6、@within

使用 @within(注解类型) 匹配所有持有指定注解的方法;注解类型也必须是全限定类型名;

//带有指定注解的方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
@within(cn.javass.spring.chapter6.Secure);

7、@target

使用 @target(注解类型) 匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;注解类型也必须是全限定类型名;

// 任何目标对象持有Secure注解的类方法;
// 必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
@target (cn.javass.spring.chapter6.Secure);

8、@args

使用 @args(注解列表) 匹配当前执行的方法传入的参数持有指定注解的执行;注解类型也必须是全限定类型名;

// 任何一个只接受一个参数的方法,且方法运行时传入的参数持有注解 cn.javass.spring.chapter6.Secure;动态切入点,类似于arg指示符;
@args (cn.javass.spring.chapter6.Secure);

9、@annotation

使用 @annotation(注解类型) 匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名;

//当前执行方法上持有注解 cn.javass.spring.chapter6.Secure将被匹配
@annotation(cn.javass.spring.chapter6.Secure)

@within@target@args@annotation 区别:

  • @within 是匹配含有指定注解的方法
  • @target 是匹配含有指定注解的对象
  • @args :方法的参数的对象上持有该注解
  • @annotation:方法上含有指定注解

10、bean

使用 bean(Bean id或名字通配符) 匹配特定名称的Bean对象的执行方法;Spring ASP扩展的,在AspectJ中无相应概念;

// 匹配所有以Service命名(id或name)结尾的Bean
bean(*Service)

11、reference pointcut

表示引用其他命名切入点,只有 @ApectJ 风格支持,Schema风格不支持,如下所示:

定义切面

@Aspect  
public class ReferencePointcutAspect {  
    @Pointcut(value="execution(* *())")  
    public void pointcut() {}  
}

引用切面

@Before(value = "cn.javass.spring.chapter6.aop.ReferencePointcutAspect.pointcut()")  
public void referencePointcutTest2(JoinPoint jp) {}  

五、自动获取方法参数

https://blog.csdn.net/angsu7023/article/details/102126558

  • 自动获取:通过切入点表达式可以将相应的参数自动传递给通知方法,例如前边章节讲过的返回值和异常是如何传递给通知方法的。

在Spring AOP中,除了execution和bean指示符不能传递参数给通知方法,其他指示符都可以将匹配的相应参数或对象自动传递给通知方法。

1、案例

(1)aop

@Aspect
@Component
public class TestAop {

    @Pointcut("@annotation(com.rewind.code.config.AopLog)")
    public void pointcut(){}

    @Pointcut(value = "args(name)", argNames = "name")
    public void pointcut2(String name){}

    @Before(value = "pointcut() && pointcut2(name)", argNames = "name")
    public void before(Object name){
        System.out.println("前置通知" + name);
    }
}

(2)目标方法

@Component
public class TestComponent2 {

    @AopLog
    public void test1(String name){
        System.out.println("test1: " + name);
    }
}

(3)调用

@Autowired
private TestComponent2 testComponent2;

public void contextLoads() {
    testComponent2.test1("lisi");
}

(4)参数传递流程

image-20220713175645540

六、案例

(1)案例一、系统日志

只要在要输出日志的方法上加上该注解,就可以将该方法设置为连接点,即对该方法进行增强

该案例为保存目标方法的执行日志

自定义注解

/**
 * 系统日志注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {

    String value() default "";
}

日志实体类

/**
 * 系统日志
 */
@Data
@TableName("sys_log")
public class SysLogEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId
    private Long id;
    //用户名(当前登录用户)
    private String username;
    //用户操作(目标方法注解上的value属性值)
    private String operation;
    //请求方法
    private String method;
    //请求参数
    private String params;
    //执行时长(毫秒)
    private Long time;
    //IP地址
    private String ip;
    //创建时间
    private Date createDate;

}

AOP

/**
 * 系统日志,切面处理类
 */
@Aspect
@Component
public class SysLogAspect {
    @Autowired
    private SysLogService sysLogService;

    @Pointcut("@annotation(io.renren.common.annotation.SysLog)")
    public void logPointCut() { 

    }

    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        long beginTime = System.currentTimeMillis();
        //执行方法
        Object result = point.proceed();
        //执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;

        //保存日志,自定义方法
        saveSysLog(point, time);

        return result;
    }

    //自定义方法
    private void saveSysLog(ProceedingJoinPoint joinPoint, long time) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        SysLogEntity sysLog = new SysLogEntity();
        SysLog syslog = method.getAnnotation(SysLog.class);
        if(syslog != null){
            //注解上的描述
            sysLog.setOperation(syslog.value());
        }

        //请求的方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        sysLog.setMethod(className + "." + methodName + "()");

        //请求的参数
        Object[] args = joinPoint.getArgs();
        try{
            String params = new Gson().toJson(args);
            sysLog.setParams(params);
        }catch (Exception e){

        }

        //获取request
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        //设置IP地址
        sysLog.setIp(IPUtils.getIpAddr(request));

        //用户名
        String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
        sysLog.setUsername(username);

        sysLog.setTime(time);
        sysLog.setCreateDate(new Date());
        //保存系统日志
        sysLogService.save(sysLog);
    }
}

(2)案例二、数据过滤

/**
 * 数据过滤注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataFilter {
    /**
     * 表的别名
     */
    String tableAlias() default "";

    /**
     * 条件的前缀,可选值[where,and]
     */
    String prefix() default "and";

    /**
     * 用户ID
     */
    String userId() default "creator";

    /**
     * 部门ID
     */
    String deptId() default "dept_id";

    /**
     * true:默认没有部门权限时也能查看自己的数据
     */
    boolean isFilterCreator() default true;

    /**
     * true:拥有子部门数据权限
     */
    boolean subDept() default false;
    /**
     * true:拥有本部门数据权限
     */
    boolean selfDept() default false;

}
/**
 * 数据过滤,切面处理类
 */
@Aspect
@Component
public class DataFilterAspect {

    @Resource
    private TROrgnizationDao trOrgnizationDao;

    @Pointcut("@annotation(io.renren.common.annotation.DataFilter)")
    public void dataFilterCut() {

    }

    @Before("dataFilterCut()")
    public void dataFilter(JoinPoint point) {
        Object params = point.getArgs()[0];
        if(params != null && params instanceof Map){
            UserDetail user = SecurityUser.getUser();

            //如果是超级管理员,则不进行数据过滤
            if(user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
                return ;
            }

            try {
                //否则进行数据过滤
                Map map = (Map)params;
                // String sqlFilter = getSqlFilter(user, point);
                // map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));
                map.put(Constant.SQL_FILTER, getSqlFilter(user, point));
            }catch (Exception e){

            }

            return ;
        }

        throw new RenException(ErrorCode.DATA_SCOPE_PARAMS_ERROR);
    }

    /**
     * 获取数据过滤的SQL
     */
    private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
        DataFilter dataFilter = method.getAnnotation(DataFilter.class);

        //1)获取表的别名
        String tableAlias = dataFilter.tableAlias();
        if(StringUtils.isNotBlank(tableAlias)){
            tableAlias +=  ".";
        }

        //2)拼接sql
        StringBuilder sqlFilter = new StringBuilder();

        //2.1)查询条件前缀
        String prefix = dataFilter.prefix();
        if(StringUtils.isNotBlank(prefix)){
            sqlFilter.append(" ").append(prefix);
        }
        sqlFilter.append(" (");

        //2.2)有数据权限的部门ID列表
        List<Long> deptIdList = user.getDeptIdList();
        //用户机构的子机构 hongzm+ 2020-4-29
        if(dataFilter.subDept()){
            List<Long> subDeptList = trOrgnizationDao.getSubDeptIdList(user.getDeptId().toString());
            deptIdList.addAll(subDeptList);
        }
        //未赋予部门权限时,默认给予本部门的权限
        // if(CollUtil.isEmpty(deptIdList)){
        //     if(deptIdList==null)
        //         deptIdList=new ArrayList<Long>();
        //     deptIdList.add(user.getDeptId());
        // }
       // 给予本部门的权限
        if(dataFilter.selfDept()){
            if(CollUtil.isEmpty(deptIdList)){
                deptIdList = new ArrayList<>();
            }
            deptIdList.add(user.getDeptId());
        }
        if(CollUtil.isNotEmpty(deptIdList)){
            sqlFilter.append(tableAlias).append(dataFilter.deptId());
            sqlFilter.append(" in(").append(StringUtils.join(deptIdList, ",")).append(")");
        }

        //2.3)查询本人数据 hongzm+ 2020-4-29
        if(dataFilter.isFilterCreator()){
            if(CollUtil.isNotEmpty(deptIdList)){
                sqlFilter.append(" or ");
            }
            sqlFilter.append(tableAlias).append(dataFilter.userId()).append("=").append(user.getId());
        }

        sqlFilter.append(")");
        return sqlFilter.toString();
    }
}

七、Advisor

1、Advisor

基本接口包含AOP通知(在连接点采取的操作)和决定通知适用性的过滤器(例如切入点)。这个接口不是供Spring用户使用的,而是为了支持不同类型通知的通用性。

Spring AOP基于通过方法拦截传递的通知,符合AOP联盟拦截API。Advisor接口允许支持不同类型的通知,例如前通知和后通知,这些通知不需要使用拦截来实现。

advisor就像一个独立的切面,只有一条advice通知。

public interface Advisor {

    // 空通知
    Advice EMPTY_ADVICE = new Advice() {};

    // 获取通知,可以是拦截器、前置通知、抛出异常通知等
    Advice getAdvice();

    // 是否和每个被通知的实例相关
    boolean isPerInstance();
}

1、Advisor:充当Advice和Pointcut的适配器,类似使用Aspect的 @Aspect 注解的类。一般有advice和pointcut属性。顶层接口为 org.springframework.aop.Advisor,应用中可直接使用org.springframework.aop.support.DefaultPointcutAdvisor

2、Advice:用于定义拦截行为,顶层接口为 org.aopalliance.aop.Advice,该接口只是标识接口,应用中可直接实现BeforeAdvice ,ThrowsAdvice,MethodInterceptor ,AfterReturningAdvice ,IntroductionInterceptor 等子接口

3、Pointcut:用于定义拦截目标集合,祖先接口为org.springframework.aop.Pointcut

image-20230426165613722

2、DefaultPointcutAdvisor

DefaultPointcutAdvisor是Spring默认的一个很强大的增强类,使用配置类自动注入,在配置类中获取切点的配置

public class DefaultPointcutAdvisor 
                    extends AbstractGenericPointcutAdvisor 
                    implements Serializable {

    private Pointcut pointcut;

    public DefaultPointcutAdvisor() {
        this.pointcut = Pointcut.TRUE;
    }

    public DefaultPointcutAdvisor(Advice advice) {
        this(Pointcut.TRUE, advice);
    }

    public DefaultPointcutAdvisor(Pointcut pointcut, Advice advice) {
        this.pointcut = Pointcut.TRUE;
        this.pointcut = pointcut;
        this.setAdvice(advice);
    }

    public void setPointcut(@Nullable Pointcut pointcut) {
        this.pointcut = pointcut != null ? pointcut : Pointcut.TRUE;
    }

    public Pointcut getPointcut() {
        return this.pointcut;
    }

    public String toString() {
        return this.getClass().getName() + ": pointcut [" + this.getPointcut() + "]; advice [" + this.getAdvice() + "]";
    }
}

3、MethodInterceptor

MethodInterceptor是AOP项目中的拦截器(注:不是动态代理拦截器),区别与HandlerInterceptor拦截目标时请求,它拦截的目标是方法。

实现MethodInterceptor拦截器大致也分为两种:

(1)MethodInterceptor接口;

(2)利用AspectJ的注解配置;

3、使用步骤

之前为解决异常的统一处理方式,写了异常处理切面,但是随着微服务节点越来越多,会出现每个微服务都需要单独写一个异常处理的切面,比较麻烦,因此考虑将切点可配置,切面提取到公共包内,但是几经尝试,@Aspect的方式始终无法读取配置的切点,最终使用DefaultPointcutAdvisor解决。

(1)先定义增强的功能

public class ExceptionAop implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object proceed = null;
        try {
            proceed = invocation.proceed();
        } catch (Exception e) {
            ResultDTO resultDTO = ErrorUtil.getResultDTO(e);

            BaseException baseException = new BaseException(resultDTO.getErrorNo().toString());

            baseException.setReturnCode(resultDTO.getErrorNo().toString());
            baseException.setErrorMessage(resultDTO.getErrorInfo());
            throw baseException;
        }
        return proceed;
    }
}

(2)DefaultPointcutAdvisor

@Configuration
@ConditionalOnProperty(name = "aop.exception.pointcut")
public class AopConfig {


    private Logger logger = LoggerFactory.getLogger(AopConfig.class);

    @Value("${aop.exception.pointcut}")
    private String exceptionExecution;

    /**
     * 异常的切面
     * @return
     */
    @Bean
    public DefaultPointcutAdvisor defaultPointcutAdvisor2() {
        ExceptionAop interceptor = new ExceptionAop();
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(exceptionExecution);
        // 配置增强类advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(pointcut);
        advisor.setAdvice(interceptor);
        logger.info("Add exception aop");
        return advisor;
    }
}

(3)读取配置文件

#异常处理切点
aop.exception.pointcut=execution(* com.mumu.trade.*.impl.*.*(..))

4、案例

import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;


@Aspect
@Configuration
public class TransactionConfig {

    /**
     * 切面地址
     */
    private static final String AOP_POINTCUT_EXPRESSION = "execution(* com.rewind.demo.service.*.*(..))";
    /**
     * 事务失效时间
     */
    private static final int TX_METHOD_TIMEOUT = 5;

    @Autowired
    private TransactionManager transactionManager;

    /**
     * 事务配置
     *
     * @return
     */
    @Bean
    public TransactionInterceptor txAdvice() {

        DefaultTransactionAttribute txAttr_REQUIRED = new DefaultTransactionAttribute();
        /**
         * PROPAGATION_REQUIRED:事务隔离性为 1
         * 若当前存在事务,则加入该事务
         * 如果当前没有事务,则创建一个新的事务,这是默认值
         */
        txAttr_REQUIRED.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        /**
         * 设置事务失效时间,如果超过5秒,则回滚事务
         */
        txAttr_REQUIRED.setTimeout(TX_METHOD_TIMEOUT);

        DefaultTransactionAttribute txAttr_REQUIRED_READONLY = new DefaultTransactionAttribute();
        /**
         * transactiondefinition 定义事务的隔离级别;
         * PROPAGATION_NOT_SUPPORTED 事务传播级别5,以非事务运行,如果当前存在事务,则把当前事务挂起
         */
        txAttr_REQUIRED_READONLY.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
        /**
         * 设置当前事务是否为只读事务
         * true为只读
         */
        txAttr_REQUIRED_READONLY.setReadOnly(true);

        /**
         * 事务管理规则,声明具备事务管理的方法名
         */
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();

        source.addTransactionalMethod("save*", txAttr_REQUIRED);
        source.addTransactionalMethod("add*", txAttr_REQUIRED);
        source.addTransactionalMethod("delete*", txAttr_REQUIRED);
        source.addTransactionalMethod("update*", txAttr_REQUIRED);

        source.addTransactionalMethod("get*", txAttr_REQUIRED_READONLY);
        source.addTransactionalMethod("query*", txAttr_REQUIRED_READONLY);
        source.addTransactionalMethod("find*", txAttr_REQUIRED_READONLY);
        source.addTransactionalMethod("list*", txAttr_REQUIRED_READONLY);
        source.addTransactionalMethod("count*", txAttr_REQUIRED_READONLY);

        return new TransactionInterceptor(transactionManager, source);
    }

    /**
     * 利用AspectJExpressionPointcut设置切面=切点+通知(写成内部bean的方式)
     *
     * @return
     */
    @Bean
    public Advisor txAdviceAdvisor() {
        /**
         * 声明切点的面
         * 切面(Aspect):切面就是通知和切入点的结合。
         * 通知和切入点共同定义了关于切面的全部内容——它的功能、在何时和何地完成其功能
         */
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        /**
         * 声明和设置需要拦截的方法,用切点语言描写
         */
        pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
        /**
         * 设置切面=切点pointcut+通知TxAdvice
         */
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }
}

  目录