springboot面相切面编程-自定义注解的实现

为什么会有自定义注解的存在呢?因为一个接口可能需要执行某个动作,而有些接口不需要,自定义注解应用灵活,比如验证是否登录注解,只需要在接口上面加上自定义的注解就可以拦截,又或者一些关键性的吊用接口调用操作,比如登录,需要将日志记录到数据库,也需要自定义注解
下面以一个例子解释自定义注解的使用

1、代码

1、定义注解
package com.bootdo.clouddocommon.annotation;

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

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

2、实现这个注解
package com.bootdo.clouddocommon.aspect;

import com.bootdo.clouddocommon.annotation.Log;
import com.bootdo.clouddocommon.context.FilterContextHandler;
import com.bootdo.clouddocommon.dto.LogDO;
import com.bootdo.clouddocommon.service.LogRpcService;
import com.bootdo.clouddocommon.utils.HttpContextUtils;
import com.bootdo.clouddocommon.utils.IPUtils;
import com.bootdo.clouddocommon.utils.JSONUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.BeforeAdvice;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;

@Aspect
@Component
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Autowired
    LogRpcService logService;


    @Pointcut("@annotation(com.bootdo.clouddocommon.annotation.Log)")
    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;
        //异步保存日志
        saveLog(point, time);
        return result;
    }

    void saveLog(ProceedingJoinPoint joinPoint, long time) throws InterruptedException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogDO sysLog = new LogDO();
        Log syslog = method.getAnnotation(Log.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 = JSONUtils.beanToJson(args[0]).substring(0, 4999);
            sysLog.setParams(params);
        } catch (Exception e) {

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

        sysLog.setUserId(Long.parseLong(FilterContextHandler.getUserID() == null ? "000000" : FilterContextHandler.getUserID()));
        sysLog.setUsername(FilterContextHandler.getUsername() == null ? "" : FilterContextHandler.getUsername());
        sysLog.setTime((int) time);
        // 系统当前时间
        Date date = new Date();
        sysLog.setGmtCreate(date);
        // 保存系统日志
        logService.save(sysLog);//这里使用的是远程调用,这里不解释
    }
}
3、使用注解
 @Log("获取当前用户的菜单")
    @GetMapping("currentUserMenus")
    R currentUserMenus() {
        return R.ok().put("currentUserMenus",menuService.RouterDTOsByUserId(SecuityUtils.getCurrentUser().getId()));
    }

上面三步即可完成

2、解释

首先解释上篇文章的target注解

1、Target 注解

java.lang.annotation.Target 用于设定注解使用范围
java.lang.annotation.ElementType Target通过ElementType来指定注解可使用范围的枚举集合
具体见下表

取值 注解使用范围
METHOD 可用于方法上
TYPE 可用于类或者接口上
ANNOTATION_TYPE 可用于注解类型上(被@interface修饰的类型)
CONSTRUCTOR 可用于构造方法上
FIELD 可用于域上
LOCAL_VARIABLE 可用于局部变量上
PACKAGE 用于记录java文件的package信息
PARAMETER 可用于参数上
2 Retention注解

这个简单,表示这个注解的声明周期,周期过了就不起作用了。

类别 声明周期
RetentionPolicy.SOURCE 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
RetentionPolicy.CLASS 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
RetentionPolicy.RUNTIME 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
3 Pointcut
@Pointcut("@annotation(com.bootdo.clouddocommon.annotation.Log)")
    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;
        //异步保存日志
        saveLog(point, time);
        return result;
    }

当然这里可以不要 logPointCut()这个方法,直接这样写

@Around("@annotation(com.bootdo.clouddocommon.annotation.Log)") 
    public Object around(ProceedingJoinPoint point) throws Throwable {
        long beginTime = System.currentTimeMillis();
        // 执行方法
        Object result = point.proceed();
        // 执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        //异步保存日志
        saveLog(point, time);
        return result;
    }
4、point.proceed()方法

上面的代码中计算了每个接口执行所消耗的时间,那么程序是如何判断执行完毕的呢,这就是环绕通知的强大之处了。因为这里必须加上Object result = point.proceed();表示执行currentUserMenus这个方法,这样就可在方法执行完以后计算消耗时间

环绕通知:可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。

5 、ProceedingJoinPoint和JoinPoint

ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法

方法 解释
java.lang.Object proceed() throws java.lang.Throwable 通过反射执行目标对象的连接点处的方法
java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable 通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参

@Around是在方法执行前后执行增强,他最强大的地方在于可以替换原来的入参,改变方法的执行结果

提醒:经过反复测试,可以不使用@Around,对于before和after以及其他不产生影响,但是一旦使用@Around,必须有 Object result = point.proceed();,表示执行这个方法,并且需要 return result;将结果返回,如果没有这个,则原有的方法没有返回值。

此文章是基于demo写的,感兴趣的可以下载运行看看。
下载地址:https://download.csdn.net/download/qq_28483283/11233595
前端是vue写的,后端是springboot+springcloud
ps:本来不想要设置下载积分的,可是csdn强制要5积分,我也很无奈.

你可能感兴趣的:(springBoot)