SpringAOP三

SpringAOP项目应用

1、切入点表达式

首先是切入点表达式的书写方式,AspectJ定义了专门的表达式用于指定切入点。表达式原型是:

execution(modifiers-pattern? ret-type-pattern 
      declaring-type-pattern?name-pattern(param-pattern)
       throws-pattern?)

modifiers-pattern] :访问权限类型

ret-type-pattern :返回值类型

declaring-type-pattern :包名类名

name-pattern(param-pattern) :方法名(参数类型和参数个数)

throws-pattern:抛出异常类型

?:表示可选的部分

以上信息描述的信息是:

execution(访问权限 方法返回值 方法声明(参数) 异常类型)

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。

在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。

表达式 execution (* .add(int,..)) || execution( *.sub(int,..))
含义 任意类中第一个参数为int类型的add方法或sub方法

通过@Pointcut注解配置切入点。该注解作用在方法上。我们在编写增强方法的时候,切入点的名字就是该方法的方法名。@Pointcut注解中的切入点表达式,就是我们要对哪些方法进行增强。

/**
 * 配置切入点,编写切入点表达式. 我们要增强什么
 */
@Pointcut("execution(* com.xue.demo.service.impl.*.*(..))")
public void pointCut() {
}

/**
 * 前置通知
 */
@Before(value = "pointCut()")
public void beforeRun() {
    System.out.println("执行之前执行,前置通知");
}

这里我们对com.xue.demo.service.impl包下的所有类的所有的方法进行了增强。我们在调用com.xue.demo.service.impl包下的所有类的所有的方法前,会先执行我们的前置通知方法。

对切入点的配置,还有其他的方式。

如果我们要配置两个切入点,比如需要对两个包下的方法进行增强,我们就需要编写两个切入点表达式。因此,我们的切入点表达式支持使用&&、|| 和 !。

我们可以配置两个切入点,用两个不同的方法名。然后我们再配置一个切入点,该切入点的切入点表达式就是那两个切入点的方法名组合而成的。

@Pointcut("execution(* com.xue.demo.service.impl.*.ru*(..))")
public void pointCutR() {
}
@Pointcut("execution(* com.xue.demo.service.impl.*.ju*(..))")
public void pointCutJ() {
}
@Pointcut(value = "pointCutJ() || pointCutR()")
public void pointCut() {
}

这里配置了两个切入点,方法分别是pointCutR()和pointCutJ()。这两个方法分别对com.xue.demo.service.impl包下的所有类的ru开头的方法和com.xue.demo.service.impl包下的ju开头的方法进行了增强。我们可以再配置一个切入点,该切入点的表达式就是pointCutR()和pointCutJ()两个方法的方法名。使用 || 连接。表示 com.xue.demo.service.impl包下的所有类的ru开头的方法和ju开头的方法都会被增强。

切入点表达式:

切入点表达式主要为了帮助我们增强我们要增强的方法。根据切入点表达式,可以找到我们要增强的方法。个人理解,感觉就像是一个导航,通过切入点表达式,就可以知道哪些方法需要被增强。
切入点表达式由 访问修饰符 返回值 报名 类名 方法名 以及我们的方法的参数组成。其中访问修饰符是可以省略不写的。返回值我们可以根据我们要增强的方法进行选择。也可以使用通配符 * ,表示所有返回类型的方法全部都被增强。
切入点表达式中的包名,表示了指定包下的类需要被增强。包名也可以使用通配符 * 表示。
类名和方法名也都可以使用我们的通配符 * 表示。但是方法中的参数,我们使用的是(…)表示多个参数。还可以指定增强方法的参数类型。

在进行切入点配置的时候,支持多种配置方式。可以根据我们的需求,对指定的方法进行增强。

execution:用于匹配方法执行的连接点。
within:用于匹配指定类型内的方法执行。
this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配。
target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配。
args:用于匹配当前执行的方法传入的参数为指定类型的执行方法。
@within:用于匹配所以持有指定注解类型内的方法。
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解。
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行。
@annotation:用于匹配当前执行方法持有指定注解的方法。

对应链接:https://blog.csdn.net/Mr_97xu/article/details/115795608

2、切入点表达式的多个示例

我自己写了一个具体的例子:

public interface UserService{
    String annotate1( String name);
    String annotate2( String name);
}

// 经过实验发现,将注解标注在接口、方法参数上是无效的。方法应该放置在具体的实例之上
@Service
public class UserServiceImpl implements UserService {


    @Override
    @LogCustom
    public String annotate1( String name) {
        System.out.println("方法在参数上");
        return null;
    }

    @Override
    @LogCustom
    public String annotate2(String name) {
        System.out.println("方法在方法上");
        return null;
    }
}

@Aspect
@Component("myAdvice2")
public class MyAdvice2 {

    @Pointcut("@annotation(com.guang.springaop.annotate.LogCustom)")
    public void pointCut() {
    }

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println("---------hello,before-----"+name);
    }
}

切入点表达式示例:

execution(public void com.guang.dao.impl.UserDao.save())
execution(void com.guang.dao.impl.UserDao.*(..))
execution(* com.guang.dao.impl.*.*(..))
execution(* com.guang.dao..*.*(..))
execution(* *..*.*(..)) --不建议使用

示例一:

package com.example.ba01;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import java.util.Date;

/**
 * @Aspect:是aspectj框架中的注解。
 *      作用:表示当前类是切面类。
 *      切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *      使用位置:在类定义的上面
 */
@Aspect
@Component
public class MyAspect {
    /**
     * 定义方法,方法是实现切面功能的
     * 方法的定义要求:
     * 1.公共方法public
     * 2.方法没有返回值
     * 3.方法名称自定义
     * 4.方法可以有参数,也可以没有参数
     *   如果有参数,参数不是自定义的,有几个参数类型可以使用
     */

    /**
     * @Before:前置通知注解
     *      属性:value,是切入点表达式,表示切面的功能执行的位置
     *      位置:在方法的上面
     *  特点:
     *  1.在目标方法之前先执行
     *  2.不会改变目标放方法的执行结果
     *  3.不会影响目标方法的执行
     *
     */
    /*@Before(value = "execution(public void com.example.ba01.SomeServiceImpl.doSome(String, Integer))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
    }*/

    /*@Before(value = "execution(void *..SomeServiceImpl.doSome(String, Integer))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("1前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
    }*/

    /*@Before(value = "execution(void *..SomeServiceImpl.doSome(..))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("2前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
    }*/

    /*@Before(value = "execution(void *..SomeServiceImpl.do*(..))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("3前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
    }*/

    @Before(value = "execution(* *..SomeServiceImpl.do*(..))")
    public void myBefore(){
        //就是你切面要执行的功能代码
        System.out.println("4前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
    }

}

对于上面的方法中,可以在方法参数上添加一个参数JoinPoint。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。

/**
     * 指定通知方法中的参数:JoinPoint
     * JoinPoint:业务方法,要加入切面功能的业务方法
     *      作用:可以在通知方法中获取方法执行时的信息,例如方法名称,方法的实参
     *      如果切面功能中需要用到方法的信息,就加入JoinPoint
     *      这个JoinPoint参数的值是由框架赋予,必须是第一位置的参数
     */
    @Before(value = "execution(* *..SomeServiceImpl.do*(..))")
    public void myBefore(JoinPoint jp){
        //获取方法的完整定义
        System.out.println("方法的签名(定义)="+jp.getSignature());
        System.out.println("方法的签名(定义)="+jp.getSignature().getName());
        //获取方法的实参
        Object args[] = jp.getArgs();
        for(Object arg : args){
            System.out.println("参数="+arg);
        }
        //就是你切面要执行的功能代码
        System.out.println("4前置通知,切面功能:在目标方法之前输出执行时间:"+new Date());
    }

对应的相应信息:

方法的签名(定义)=void com.guang.springbootone.controller.LoggerController.getuser(String,Integer)
方法的签名(定义)=  getuser

后置通知

在目标方法执行之后执行,是目标方法正常执行之后进行的操作,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。

package com.example.ba02;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import java.util.Date;

/**
 * @Aspect:是aspectj框架中的注解。
 *      作用:表示当前类是切面类。
 *      切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *      使用位置:在类定义的上面
 */
@Aspect
@Component
public class MyAspect {
    /**
     * 后置通知定义方法,方法是实现切面功能的
     * 方法的定义要求:
     * 1.公共方法public
     * 2.方法没有返回值
     * 3.方法名称自定义
     * 4.方法有参数,推荐是Object,参数名自定义
     */

    /**
     * @AfterReturning:后置通知
     *      属性:1.value 切入点表达式
     *           2.returning 自定义的变量,表示目标方法的返回值的
     *             自定义变量名必须和通知方法的形参名一样。
     *      位置:在方法定义的上面
     * 特点:
     * 1.在目标方法之后执行的
     * 2.能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
     *   Object res = doOther();
     * 3.可以修改这个返回值
     *
     * 后置通知的执行
     *      Object res = doOther();
     *      myAfterReturning(res);
     */

    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
                    returning = "res")
    public void myAfterReturning(Object res){
        //Object res : 是目标方法执行后的返回值,根据返回值做切面的功能处理
        System.out.println("后置通知:在目标方法之后执行的,获取的返回值是:"+res);
    }
}

在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

package com.example.ba03;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import java.util.Date;

/**
 * @Aspect:是aspectj框架中的注解。
 *      作用:表示当前类是切面类。
 *      切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *      使用位置:在类定义的上面
 */
@Aspect
public class MyAspect {
    /**
     * 环绕通知方法的定义格式
     * 1.public
     * 2.必须有一个返回值,推荐使用Object
     * 3.方法名称自定义
     * 4.方法有参数,固定的参数 ProceedingJoinPoint
     */

    /**
     * @Around 环绕通知
     *      属性:value 切入点表达式
     *      位置:在方法的定义上面
     * 特点:
     *      1.它是功能最强的通知
     *      2.在目标方法的前和后都能增强功能
     *      3.控制目标方法是否被调用执行
     *      4.修改原来的目标方法的执行结果,影响最后的调用结果
     *
     * 环绕通知,等同于jdk动态代理的 InvocationHandler接口
     *
     * 参数:ProceedingJoinPoint 就等同于Method
     *      作用:执行目标的方法
     * 返回值:就是目标方法的执行结果,可以被修改
     *
     * 环绕通知:经常做事务,在目标方法之前开启事务,执行目标方法,在目标方法之后提交事务
     */
    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjd) throws Throwable {
        //实现环绕通知
        Object result = null;
        System.out.println("环绕通知:在目标方法之前,输出时间" +new Date());
        //1.目标方法的调用
        result = pjd.proceed(); //相当于method.invoke(); Object result = doFirst();
        System.out.println("环绕通知:在目标方法之后,提交事务");

        //2.在目标方法的前或者后加入功能
        if(result != null){
            result = "Hello AspectJ AOP";
        }

        //返回值目标方法的执行结果
        return result;
    }
}

还可以有参数的来进行执行:

    @Around(value = "pointCut()")
    public Object around(JoinPoint pjp) throws Throwable {
        System.out.println("环绕:前置通知...");
        Object[] args = pjp.getArgs();
        System.out.println("获取得到参数值对象"+ Arrays.asList(args).toString()); // [hello, 22]
        System.out.println("获取得到被代理的对象"+pjp.getTarget()); // com.guang.springaop.service.impl.UserServiceImpl@74455848
        Signature signature = pjp.getSignature();
        Class declaringType = signature.getDeclaringType();
        System.out.println("默认的类是:"+declaringType); // interface com.guang.springaop.service.UserService
        System.out.println("默认类型的名称"+signature.getDeclaringTypeName()); // com.guang.springaop.service.UserService
        System.out.println("获取得到方法签名"+ pjp.getSignature()); // String com.guang.springaop.service.UserService.sayHello(String,String)
        //切入点方法执行
        ProceedingJoinPoint joinPoint = (ProceedingJoinPoint)pjp;
        Object proceed = joinPoint.proceed(args);
        System.out.println("环绕:后置通知...");
        return proceed;
    }

使用对象来进行操作:

    @Around(value = "pointCut()")
    public Object around(JoinPoint pjp) throws Throwable {
        System.out.println("环绕:前置通知...");
        Object[] args = pjp.getArgs();
        System.out.println("获取得到参数值对象"+ Arrays.asList(args).toString()); // [hello, 22]
        System.out.println("获取得到被代理的对象"+pjp.getTarget()); // com.guang.springaop.service.impl.UserServiceImpl@74455848
        Signature signature = pjp.getSignature();
        Class declaringType = signature.getDeclaringType();
        System.out.println("默认的类是:"+declaringType); // interface com.guang.springaop.service.UserService
        System.out.println("默认类型的名称"+signature.getDeclaringTypeName()); // com.guang.springaop.service.UserService
        System.out.println("获取得到方法签名"+ pjp.getSignature()); // String com.guang.springaop.service.UserService.sayHello(String,String)
        //切入点方法执行
        ProceedingJoinPoint joinPoint = (ProceedingJoinPoint)pjp;
        Object proceed = joinPoint.proceed(args);
        System.out.println("环绕:后置通知...");
        return proceed;
    }

我想在所有的controller在接收到对应的参数的时候来打印请求进来的参数:

package com.guang.springbootone.aspect;

import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

@Aspect
@Component
public class AopLogAspect {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * execution 指定controller
     * !execution 排查指定controller下的方法

     @Pointcut("(execution(public * com.guang.springbootone.controller.*.*(..)) && !execution(public * com.guang.springbootone.controller.CdpUploadController.upload(..)))")
     public void aopLogAspect() {
     }
     */

    /**
     * 还可以引入其他类中的方法来进行执行
     */
    @Pointcut("(execution(public * com.guang.springbootone.controller.Log*.*(..)))")
    public void aopLogAspect() {
    }

    /**
     * 这里我觉得使用环切是不合适的
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Before(value = "aopLogAspect()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        logger.info("请求开始 : ====================================================================================");
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 记录下请求内容
        logger.info("请求类型 :" + request.getMethod() + "  " + "请求URL : " + request.getRequestURL());
        logger.info("请求IP  : " + request.getRemoteAddr());
        logger.info("请求方法 : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        LocalVariableTableParameterNameDiscoverer realParamNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames = realParamNameDiscoverer.getParameterNames(method);
        //方法 1 请求的方法参数值 JSON 格式 null不显示
        Object[] args = joinPoint.getArgs();
        if (args.length > 0 && joinPoint.getArgs().length > 0) {
            for (int i = 0; i < args.length; i++) {
                //请求参数类型判断过滤,防止JSON转换报错
                if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse || args[i] instanceof MultipartFile) {
                    continue;
                }
                logger.info("请求参数名称 :" + paramNames[i] + ", 内容 :" + JSON.toJSONString(args[i]));
            }
        }
    }

    /**
     * 如果是流数据,不应该将其打印出来!做个判断
     *
     * @param ret
     * @throws Throwable
     */
    @AfterReturning(returning = "ret", pointcut = "aopLogAspect()")
    public void doAfterReturning(Object ret) throws Throwable {
        // 处理完请求,返回内容
        logger.info("返回内容 : " + JSON.toJSONString(ret));
        logger.info("请求结束 :====================================================================================");
    }

}

3、在SpringBoot中使用纯注解开发

导入aop坐标

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

编写配置类,开启自动AOP自动代理和包扫描(@SpringBootApplication中已经指定),下面说的是在Spring项目中来进行开发的。

@Configuration //标记当前类是:配置类
@ComponentScan(basePackage="com.itheima") //配置注解扫描
@EnableAspectJAutoProxy //开启AOP自动代理
public class AppConfig{   
}
posted @ 2022-06-30 17:40  雩娄的木子  阅读(38)  评论(0编辑  收藏  举报