您的当前位置:首页正文

springboot自定义注解实战

2024-11-23 来源:个人技术集锦

基础知识概述

aop 面向切面编程

1、切面(aspect)
散落在系统各处的通用的业务逻辑代码,如日志模块,权限模块,事务模块等,切面用来装载pointcut和advice
2、通知(advice)
所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类

  • Before advice:在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。
  • After advice:当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
  • After returnadvice:在某连接点正常完成后执行的通知,不包括抛出异常的情况。
  • Around advice:包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法执行的前后实现逻辑,也可以选择不执行方法
  • Afterthrowing advice:在方法抛出异常退出时执行的通知。

3、连接点(joinpoint)
被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
4、切入点(pointcut)
拦截的方法,连接点拦截后变成切入点
6、目标对象(Target Object)
代理的目标对象,指要织入的对象模块
7、织入(weave)
通过切入点切入,将切面应用到目标对象并导致代理对象创建的过程
8、AOP代理(AOP Proxy)
AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理

元注解

元注解:修饰注解的注解
@Target:注解的作用目标
@Retention:注解的生命周期
@Documented:注解是否应当被包含在 JavaDoc 文档中
@Inherited:是否允许子类继承该注解

@Target:用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。语法如下:

@Target(value = {ElementType.METHOD})

@Retention: 注解指定了被修饰的注解的生命周期。语法如下:
@Retention(value = RetentionPolicy.RUNTIME)

RetentionPolicy.SOURCE:注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视
RetentionPolicy.CLASS:注解只被保留到编译进行的时候,不会被加载到JVM中
RetentionPolicy.RUNTIME:注解可以保留到程序运行的时候,会被加载到JVM中,所以程序运行时可以获取到它


剩下两种类型的注解我们日常用的不多,也比较简单,需要知道他们各自的作用即可:

@Documented 注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。
@Inherited 注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。

实现步骤:


1、定义一个切面类Aspect
声明一个切面类,增加@Component和@Aspect两个注解,同时SpringBoot要引入spring-boot-stater-aop依赖包。
2、定义切点Pointcut
定义切点,并定义切点在哪些地方执行,采用@Pointcut注解完成,如@Pointcut(public * com.xxx.xxx..(…))
规则:修饰符(可以不写)+返回类型+包下的类+方法+方法参数 “”代表不限,“…”两个点代表参数不限,例如:
切点名称myPointcut,在返回类型不限的com.binlog.study.aop.controller包下的所有类,所有方法并且参数不限。
参考:@Pointcut(value="execution(
com.binlog.study.aop.controller..(…))")
3、定义Advice通知
利用通知的5种类型注解@Before、@After、@AfterReturning、@AfterThrowing、@Around来完成在某些切点的增强动作。

springboot 增加 pom

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

如果是初学者,可以看这个简单的例子;
下面第二个例子是进阶版

实战

第一个例子

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String detail() default "";
}

// LogAspect
@Aspect
@Component
public class LogAspect {
    /**
     * 此处的切点是注解的方式,也可以用包名的方式达到相同的效果
     */
    @Pointcut("@annotation(com.aop.Log)")
    public void operationLog(){}

    // 环绕增强
    @Around("operationLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object res = null;
        try {
            res =  joinPoint.proceed();
            return res;
        } finally {
            try {
                System.out.println("方法执行后打印日志");
            }catch (Exception e){
                System.out.println("LogAspect 操作失败:" + e.getMessage());
                e.printStackTrace();
            }
        }
    }
    /**
     * 处理完请求,返回内容
     */
    @AfterReturning(returning = "ret", pointcut = "operationLog()")
    public void doAfterReturning(Object ret) {
        System.out.println("方法的返回值 : " + ret);
    }

    /**
     * 后置异常通知
     */
    @AfterThrowing("operationLog()")
    public void throwss(JoinPoint jp){
        System.out.println("方法异常时执行.....");
    }


    /**
     * 后置最终通知,final增强,不管是抛出异常或者正常退出都会执行
     */
    @After("operationLog()")
    public void after(JoinPoint jp){
        System.out.println("方法最后执行.....");
    }

}


// controller 
@RestController
@RequestMapping("user")
public class Controller {
    @Autowired
    private UserService  userService;

    @GetMapping("/findUserNameByTel")
    public String findUserNameByTel(@RequestParam("tel") String tel){
        return userService.findUserName(tel);
    }
}


// UserService 
@Service
public class UserService {
    @Log(detail = "通过手机号[{{tel}}]获取用户名")
    public String findUserName(String tel) {
        System.out.println("tel:" + tel);
        return "zhangsan";
    }
}

第二个例子

自定义注解打印日志和抛出异常信息

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestAnnotate {
    String value() default "";
}

切面类

@Aspect
@Slf4j
@Component
public class RequestAspet {
    // PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名
    // 切面最主要的就是切点,所有的故事都围绕切点发生
    // logPointCut()代表切点名称
    @Pointcut(value = "@annotation(RequestAnnotate)")
    private void requestLog() {
    }

    //定义切点
    @Pointcut(value="execution(* com.example.demo.controller.*.*(..))")
    public void operErrorPointCut() { }


    /**
     * 执行切点之前
     * @param joinPoint
     */
    @Before(value = "requestLog()")
    private void doBefore(JoinPoint joinPoint) throws ClassNotFoundException {
        log.info("执行前置");
        try {
            // 获取被代理对象的类名
            String targetName = joinPoint.getTarget().getClass().getName();
            // 获取 Signature 对象,包含目标方法名和所属类的 class 信息
            String methodName = joinPoint.getSignature().getName();
            // 获取方法参数
            Object[] arguments = joinPoint.getArgs();
            // 根据类名获取类
            Class<?> targetClazz = Class.forName(targetName);
            Method[] methods = targetClazz.getMethods();
            String operation = "";
            //遍历方法名和参数长度一致的方法
            for (Method m :methods){
                // 对比方法名一致
                if (methodName.equals(m.getName())){
                    Class<?>[] clazz = m.getParameterTypes();
                    // 参数长度一致
                    if (clazz.length == arguments.length){
                        // 获取的是方法上注解后面的 value 值,可以作为注释传入
                        operation = m.getAnnotation(RequestAnnotate.class).value();
                        break;
                    }

                }
            }
            StringBuilder sb = new StringBuilder();
            for (Object arg : arguments) {
                sb.append(arg);
                sb.append("&");
            }

            // *========控制台输出=========*//
            log.info("[X用户]执行了[" + operation + "],类:" + targetName + ",方法名:" + methodName + ",参数:" + sb.toString());

        } catch (Throwable e){
            log.info("around " + joinPoint + " with exception : " + e.getMessage());
        }

    }

    /**
     * 执行切点之后
     * @param joinPoint
     */
    @After(value = "requestLog()")
    private void doAfter(JoinPoint joinPoint) {
        log.info("执行后置");
        try {
            String targetName = joinPoint.getTarget().getClass().getName();
            String targetMethod = joinPoint.getSignature().getName();
            Object[] args = joinPoint.getArgs();
            Class<?> targetClazz = Class.forName(targetName);
            Method[] methods = targetClazz.getMethods();
            String operation = "";
            for (Method m :methods){
                if (targetMethod.equals(m.getName())){
                    if (m.getParameterTypes().length == args.length){
                        operation= m.getAnnotation(RequestAnnotate.class).value();
                    }
                }
            }
            StringBuilder sb = new StringBuilder();
            for (Object arg:args){
                sb.append(arg);
                sb.append("&");
            }
            log.info("用户执行了 operation " + operation + "类:" + targetName + "方法名:" + targetMethod + "参数:" +sb.toString() );
        }catch (Throwable e){
            log.info("around " + joinPoint + " with exception : " + e.getMessage());

        }

    }
}

controller

    @RequestMapping("test")
    @RequestAnnotate(value = "测试")
    public void test() {
        System.out.println("执行测试");
    }

测试结果:

异常接口:

    @RequestMapping("test")
    @RequestAnnotate(value = "测试")
    public void test() {
        System.out.println("执行测试");
        int[] array = {1,2};
        System.out.println(array[2]);
    }

显示全文