spring aop (面向切面编程)实现事务管理(^_^^_^)

事务嵌套,不回滚的问题描述:

要想事务起作用,必须是主方法名上有@Transactional注解,方法体内不能用try catch;如果用try catch,则catch中必须用throw new RuntimeException();

@Transactional注解应该只被应用到public方法上,不要用在protected、private等方法上,即使用了也将被忽略,不起作用。这是由Spring AOP决定的。

只有来自外部的方法调用才会呗AOP代理捕捉,类内部方法调用类内部的其他方法,子方法并会不引起事务行为,即使被调用的方法上使用有@Transactional注解。

类内部方法调用内部的其他方法,被调用的方法体中如果有try catch,则catch中必须用throw new RuntimeException(),否则即使主方法上加上@Transactional注解,如果被调用的子方法出错也不会抛出异常,不会引起事务起作用。

一、不同类中的方法的事务嵌套问题:

不同类中有@Transactional 的方法调用有@Transactional 的方法

二、同一类中的方法的事务嵌套

同一类中的其他没有@Transactional 注解的方法内部调用有@Transactional 注解的方法,有@Transactional 注解的方法的事务被忽略,不会发生回滚。

原因:spring的事务处理底层是通过CGlib动态代理来实现的。目标方法由外部调用才由 Spring 生成的代理对象来管理,这会造成自调用问题。

为了解决事务嵌套这个问题,我们用到了@AspectJ 面向切面来实现,不能使用spring aop动态代理

AOP的应用场景: 
权限控制、缓存控制、事务控制、审计日志、性能监控、分布式追踪、异常处理

Spring AOP的使用方式包含XML配置和注解方式(不推荐),下面看一下基于注解方式的AOP。

AOP的注解主要包括@Aspect、@Pointcut、Advice三种。在详细记录前先给出一个AOP的简单使用代码(流程:引入依赖–>定义切面类–>在切面类中定义Advice以及前后逻辑),如果要获取方法的一些属性(比如方法名,返回值、参数等等),除了环绕通知需要传入ProceedingJoinPoint对象,其他的Advice则是需要传入JoinPoint对象,两类不同!

1.@Aspect
主要用来标注Java类,表明它是一个切面配置的类,通常下面也会加上@Component注解来表明它由Spring管理

2.@Pointcut
主要有pointCutExpression(切面表达式)来表达,用来描述你要在哪些类的哪些方法上注入代码。其中切面表达式包含了designators(指示器,主要描述通过哪些方式去匹配Java类的哪些方法:如execution()等)和wildcards(通配符:如*)以及operators(运算符:如&&、||、!),具体如下所示 
åé¢è¡¨è¾¾å¼ç»æ

designators指示器 
表示想要通过什么样的方式匹配想要的方法,具体组成如下图,重点是execution() 

æ示å¨çç»æ

1.匹配包/类型within()

//匹配ProductService类种的所有方法
 

//匹配ProductService类种的所有方法
@Poincut("within(com.hhu.service.ProductService)")
public void matchType(){
    ...
}

//匹配com.hhu包及其子包下所有类的方法
@Pointcut("within(com.hhu..*)")
public void matchPackage(){
    ...
}

2.//匹配com.hhu包及其子包下所有类的方法

//匹配AOP对象的目标对象为指定类型的方法,即DemoDao的aop的代理对象
@Pointcut("this(com.hhu.DemaoDao)")
public void thisDemo() {
    ...
}

//匹配实现IDao接口的目标对象(而不是aop代理后的对象,这里即DemoDao的方法)
@Pointcut("target(com.hhu.Idao)")
public void targetDemo() {
    ...
}

//匹配所有以Service结尾的bean中的方法
@Pointcut("bean(*Service)")
public void beanDemo() {
    ...
}

 3.参数匹配(主要有execution()和args()方法)

//过滤出第一个参数是long类型的并且在com.hhu.service包下的方法,如果是第一个参数是Long,第二个参数是String则可以写成args(Long,String),如果匹配第一个为Long,其它任意的话则可以写成args(Long..)
@Pointcut("args(Long) && within(com.hhu.service.*)")
public void matchArgs() {
    ...
}

@Before("mathArgs()")
public void befor() {
    System.out.println("");
    System.out.println("###before");
}

4.匹配注解

//匹配方法标注有AdminOnly注解的方法
@Pointcut("@annotation(com.hhu.demo.security.AdminOnly)")
public void annoDemo() {
    ...
}

//匹配标注有Beta的类下的方法,要求annotation的RetentionPplicy级别为CLASS
@Pointcut("@within(com.google.common.annotations.Beta)")
public void annoWithinDemo() {
    ...
}

//匹配标注有Repository类下的方法,要求的annotation的RetentionPolicy级别为RUNTIME
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void annoTargetDemo() {
    ...
}

//匹配传入的参数类型标注有Repository注解的方法
@Pointcut("@args(org.springframework.stereotype.Repository)")
public void annoArgsDemo() {
    ...
}

 5.execution() 
标准的execution表达式如下,共有5个参数:

execution(
//权限修饰符(如public、private)
[modifier-pattern],
//返回值(如*表示任意返回值,void表示无返回值)
ret-type-pattern,
//包名(如com.hhu.service.*Service.*(..)表示对应包com.hhu.service中以Service结尾的类中的任意方法(该方法可以带任意参数),如果匹配无参的则可以写成(),如果像拦截产生异常的方法,则可以写成(..) throws+具体异常 )
[declaring-type-pattern],
name-pattern(或者是param-pattern),
[throws-pattern]
)

其中带有方括号的参数表示可以缺省。比如

@Pointcut("execution(* *..find*(Long)) && within(com.jaryle..*) ")

wildcard主要包括常用的三种: 
*表示匹配任意数量的字符 
+表示匹配制定类及其子类 
..表示一般用于匹配任意数的子包或参数。

operators主要包括如下的三种: 
&&表示与操作 
||表示或操作 
!表示非操作

3.Advice注解

表示代码织入的时机,如执行之前、执行之后等等,主要有5种,通常是进行注解切面注解后会紧接着写Advice,格式为Advice(“切面拦截方法”)。

  1. @Before,前置通知
  2. @After,后置通知,不管代码是成功还是抛出异常,都会织入
  3. @AfterReturning,返回通知,当且仅当方法成功执行
  4. @AfterThrowing,异常通知,当且仅当方法抛出异常
  5. @Around,环绕通知,基本包含了上述所有的植入位置
<!-- 添加AOP的依赖 -->
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
 </dependency>
package com.jaryle.springboot_mybatis.aop;/**
 * Create by jaryle on 2019/3/23.<br>
 */

import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;



import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * 描述:
 *    spring 面向切面Aop
 *    Aspect注解方式,个人觉得这种方法才比较灵活,与配置与工程整个代码都没有耦合(你添加一个类,做几个注解就可以用了,无需在其他地方再做什么),更易应用。
 *
 * @author jaryle
 * @create 2019-03-23 
 * @version V1.0
 */
@Component //交给Spring管理
@Aspect //表明它是一个切面类
@Order(-99) // 控制多个Aspect的执行顺序,越小越先执行
public class AopConfiguration {
    //  //定义拦截GirlController中所有public方法
//  @Before("execution(public * com.jaryle.springboot_mybatis.controller.*(..))")
//  public void log() {
//      System.out.println("******拦截前的逻辑******");
//  }
//
//  @After("execution(public * com.jaryle.springboot_mybatis.controller.*(..))")
//  public void doAfter() {
//      System.out.println("******拦截后的逻辑******");
//  }

    /*
     * 上面拦截的方式比较繁琐,因为Before和After的匹配规则有重复代码
     *
     * 可以先定义一个Pointcut,然后直接拦截这个方法即可
     *
     */
    //这里就定义了一个总的匹配规则,以后拦截的时候直接拦截log()方法即可,无须去重复写execution表达式
    @Pointcut("execution(public * com.jaryle.springboot_mybatis.controller.*(..))")
    public void log() {

    }

    /**
     * @Before,前置通知
     * @After,后置通知,不管代码是成功还是抛出异常,都会织入
     * @AfterReturning,返回通知,当且仅当方法成功执行
     * @AfterThrowing,异常通知,当且仅当方法抛出异常
     * @Around,环绕通知,基本包含了上述所有的植入位置
     */
    @Before("log()")
    //还可以传递参数
//    @Before("log() && args(productId)")
    public void doBefore(JoinPoint joinPoint) {

        System.out.println("******拦截前的逻辑******");
        System.out.println("我是前置通知!!!");
        //获取目标方法的参数信息
        Object[] obj = joinPoint.getArgs();
        //AOP代理类的信息
        joinPoint.getThis();
        //代理的目标对象
        joinPoint.getTarget();
        //用的最多 通知的签名
        Signature signature = joinPoint.getSignature();
        //代理的是哪一个方法
        System.out.println(signature.getName());
        //AOP代理类的名字
        System.out.println(signature.getDeclaringTypeName());
        //AOP代理类的类(class)信息
        signature.getDeclaringType();
        //获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        //如果要获取Session信息的话,可以这样写:
        //HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
        Enumeration<String> enumeration = request.getParameterNames();
        Map<String,String> parameterMap = new HashMap();
        while (enumeration.hasMoreElements()){
            String parameter = enumeration.nextElement();
            parameterMap.put(parameter,request.getParameter(parameter));
        }
        String str = JSON.toJSONString(parameterMap);
        if(obj.length > 0) {
            System.out.println("请求的参数信息为:"+str);
        }

    }

    @After("log()")
    public void doAfter() {
        System.out.println("******拦截后的逻辑******");
    }
    //@AfterReturning注解可以获取方法的返回值,returning的值result代表的就是返回值,形参Object类表示
    @AfterReturning(value="log()",returning="result")
    public void after(java.lang.Object result) {
        System.out.println("###after");
    }

    /**
     * 后置返回通知
     * 这里需要注意的是:
     *      如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
     *      如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
     * returning 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
     * @param joinPoint
     * @param keys
     */
    @AfterReturning(value = "execution(* com.jaryle.springboot_mybatis.controller..*.*(..))",returning = "keys")
    public void doAfterReturningAdvice1(JoinPoint joinPoint,Object keys){

        System.out.println("第一个后置返回通知的返回值:"+keys);
    }

    /**
     * 后置异常通知
     *  定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法;
     *  throwing 限定了只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行,
     *      对于throwing对应的通知方法参数为Throwable类型将匹配任何异常。
     * @param joinPoint
     * @param exception
     */
    @AfterThrowing(value = " log()",throwing = "exception")
    public void doAfterThrowingAdvice(JoinPoint joinPoint,Throwable exception){
        //目标方法名:
        System.out.println(joinPoint.getSignature().getName());
        if(exception instanceof NullPointerException){
            System.out.println("发生了空指针异常!!!!!");
        }
    }

    /**
     * 后置最终通知(目标方法只要执行完了就会执行后置通知方法)
     * @param joinPoint
     */
    @After("log()")
    public void doAfterAdvice(JoinPoint joinPoint){

        System.out.println("后置通知执行了!!!!");
    }

    /**
     * 环绕通知:
     *   环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
     *   环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
     */
    @Around("execution(* com.jaryle.springboot_mybatis.controller..*.*(..))")
    public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
        System.out.println("环绕通知的目标方法名:"+proceedingJoinPoint.getSignature().getName());
        try {//obj之前可以写目标方法执行前的逻辑
            Object obj = proceedingJoinPoint.proceed();//调用执行目标方法
            return obj;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }

}

猜你喜欢

转载自blog.csdn.net/jaryle/article/details/88764751
今日推荐