SpringBoot+AOP+自定义注解,实现日志记录/权限验证

自定义注解简介

为什么要通过AOP来实现日志记录

在业务处理中,我们经常需要将一些用户操作、或系统日志记录到数据库中,并在后台做展示。一般情况下我们需要在每个需要进行记录的业务方法中做sql操作,这样一来日志记录这种非业务层面的代码就会和业务代码耦合,显得非常难看。那么有没有一种优雅记录日志的办法呢?当然是有的,以下介绍一种基于自定义注解的使用AOP来记录日志的办法。

描述

注解是一种能被添加到java源代码中的元数据,单独使用注解,就相当于在类、方法、参数和包上加上一个装饰,什么功能也没有,仅仅是一个标志,然后这个标志可以加上一些自己定义的参数。然后就可以使用这个注解了,加在我们需要装饰的方法上,但是什么功能也没有,就像下面这样。

package com.test.boot.aop;

import java.lang.annotation.*;

/**
 * @Author: laz
 * @CreateTime: 2022-12-28  16:32
 * @Version: 1.0
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {

    String value() default "";

}

自定义注解介绍

  • 修饰符:访问修饰符必须为public,不写默认为pubic
  • 关键字:关键字为@interface
  • 注解名称:注解名称为自定义注解的名称,例如上面的MyLog 就是注解名称
  • 注解类型元素:注解类型元素是注解中内容,根据需要标志参数,例如上面的注解的value

创建自定义注解前需要了解两个注解:@Target ,@Retention

@Target——用于描述注解的使用范围,该注解可以使用在什么地方

Target类型描述
ElementType.TYPE应用于类、接口(包括注解类型)、枚举
ElementType.FIELD应用于属性(包括枚举中的常量)
ElementType.METHOD应用于方法
ElementType.PARAMETER应用于方法的形参
ElementType.CONSTRUCTOR应用于构造函数
ElementType.LOCAL_VARIABLE应用于局部变量
ElementType.ANNOTATION_TYPE应用于注解类型
ElementType.PACKAGE应用于包

@Retention——表明该注解的生命周期

生命周期类型描述
RetentionPolicy.SOURCE编译时被丢弃,不包含在类文件中
RetentionPolicy.CLASSJVM加载时被丢弃,包含在类文件中,默认值
RetentionPolicy.RUNTIME由JVM 加载,包含在类文件中,在运行时可以被获取到

本篇文章主要介绍如何简单使用aop实现日志记录以及权限验证
源码地址:https://gitee.com/lianaozhe/springboot-aop

AOP实现日志记录

1.导入依赖

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

2.创建自定义注解

package com.test.boot.aop;

import java.lang.annotation.*;

/**
 * @Author: laz
 * @CreateTime: 2022-12-28  16:32
 * @Version: 1.0
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {

    String value() default "";

}

3.编写切面类

package com.test.boot.annotations;

import com.test.boot.aop.MyLog;
import com.test.boot.entity.SysLog;
import com.test.boot.mapper.SysLogMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
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.lang.reflect.Method;
import java.util.Date;

/**
 * @Author laz
 * @Description
 * @Date 2022-12-28 10:42
 * @Version 1.0
 *
 * 第一步:明一个切面类
 */
@Slf4j
@Aspect
@Component
public class LogAspect {


    @Autowired
    private SysLogMapper sysLogMapper;

    /**
     * 用于记录方法执行时间
     */
    ThreadLocal<Long> startTime = new ThreadLocal<>();

    /**
     * 第二步:定义一个切入点,含义:只对MyLog注解生效
     */
    @Pointcut(value="@annotation(com.test.boot.aop.MyLog)")
    public void MyLogPointcut(){ }


    /**
     * 第三步:定义处理事件
     * @param joinPoint: 连接点(可以在这个类中获取对应的注解参数和方法参数)
     */
    @Before(value ="MyLogPointcut()")
    public void logTest(JoinPoint joinPoint){
        startTime.set(System.currentTimeMillis());

    }


    /**
     * 返回通知
     * @param ret
     * @throws Throwable
     */
    @AfterReturning(value = "MyLogPointcut()",returning = "ret")
    public void doAfterReturning(JoinPoint joinPoint,Object ret) throws Throwable {


        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        // 从切面织入点处通过反射机制获取织入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点所在的方法
        Method method = signature.getMethod();
        // 获取注解操作
        MyLog myLog = method.getAnnotation(MyLog.class);


        Object[] args = joinPoint.getArgs();
        String declaringTypeName = signature.getDeclaringTypeName();
        String name = signature.getName();
        String ip = getIp(request);
        SysLog sysLog = new SysLog();
        sysLog.setContent(myLog.value());
        sysLog.setMethod(name);
        sysLog.setRequestMethod(request.getMethod());
        sysLog.setRequestParam(JSONArray.fromObject(args).toString());
        sysLog.setResponseResult(ret.toString());
        //用户名在真实环境中由工具类获取或者从当前登录者的账号信息中获取,这里只是测试
        sysLog.setOperName("测试");
        sysLog.setIp(getIp(request));
        sysLog.setRequestUrl(request.getRequestURL().toString());
        sysLog.setOperTime(new Date());
        sysLog.setStatus(0);
        Long takeTime = System.currentTimeMillis() - startTime.get();
        sysLog.setTakeTime(takeTime);
        sysLogMapper.insert(sysLog);

        log.info("请求参数:{}",args);
        log.info("调用类名:{}",declaringTypeName);
        log.info("方法名称: {}",name);
        log.info("接口名称: {}",myLog.value());
        log.info("请求ip: {}",ip);
        log.info("请求方式: {}",request.getMethod());
        log.info("请求uri: {}",request.getRequestURI());
        log.info("方法的返回值 : [{}]",ret);
    }

    /**
     * 异常通知
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(throwing = "e", pointcut = "MyLogPointcut()")
    public void throwss(JoinPoint joinPoint,Exception e){


        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        // 从切面织入点处通过反射机制获取织入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点所在的方法
        Method method = signature.getMethod();
        // 获取操作
        MyLog myLog = method.getAnnotation(MyLog.class);


        Object[] args = joinPoint.getArgs();
        String name = signature.getName();
        SysLog sysLog = new SysLog();
        sysLog.setContent(myLog.value());
        sysLog.setMethod(name);
        sysLog.setRequestMethod(request.getMethod());
        sysLog.setRequestParam(JSONArray.fromObject(args).toString());
        //用户名在真实环境中由工具类获取或者从当前登录者的账号信息中获取,这里只是测试
        sysLog.setOperName("测试");
        sysLog.setIp(getIp(request));
        sysLog.setRequestUrl(request.getRequestURL().toString());
        sysLog.setOperTime(new Date());
        sysLog.setStatus(1);
        sysLog.setErrorMsg(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));
        Long takeTime = System.currentTimeMillis() - startTime.get();
        sysLog.setTakeTime(takeTime);
        sysLogMapper.insert(sysLog);
    }

    /**
     * 转换异常信息为字符串
     */
    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
        StringBuffer strbuff = new StringBuffer();
        for (StackTraceElement stet : elements) {
            strbuff.append(stet + "\n");
        }
        String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
        message = substring(message,0 ,2000);
        return message;
    }


    //字符串截取
    public static String substring(String str, int start, int end) {
        if (str == null) {
            return null;
        } else {
            if (end < 0) {
                end += str.length();
            }

            if (start < 0) {
                start += str.length();
            }

            if (end > str.length()) {
                end = str.length();
            }

            if (start > end) {
                return "";
            } else {
                if (start < 0) {
                    start = 0;
                }

                if (end < 0) {
                    end = 0;
                }
                return str.substring(start, end);
            }
        }
    }

    /**
     * 后置通知
     */
    @After("MyLogPointcut()")
    public void after(){
        log.info("后置通知.....");
    }


    //根据HttpServletRequest获取访问者的IP地址
    public static String getIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

4.编写测试接口

package com.test.boot.controller;

import com.test.boot.aop.Auth;
import com.test.boot.aop.MyLog;
import com.test.boot.enums.Role;
import com.test.boot.utils.ResultResponse;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: laz
 * @CreateTime: 2022-12-28  16:38
 * @Version: 1.0
 */
@RestController
@RequestMapping("test")
public class TestController {

    @RequestMapping("testLogAnnotation/{id}")
    @MyLog(value = "测试日志注解接口")
    public ResultResponse testLogAnnotation(@PathVariable("id")int id){
        return ResultResponse.success("id:"+id);
    }
}

5.测试

启动项目,调用该接口,查看输出日志:

在这里插入图片描述
数据库记录:

在这里插入图片描述

注:sql文件已提交至:https://gitee.com/lianaozhe/springboot-aop

AOP实现权限验证

1.创建自定义注解

package com.test.boot.aop;

import com.test.boot.enums.Role;

import java.lang.annotation.*;


/**
 * @Author: laz
 * @CreateTime: 2022-12-28  16:32
 * @Version: 1.0
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auth {

    Role [] value();
}

Role类:

package com.test.boot.enums;

/**
 * 小程序角色
 * @author chengfengluo
 * @date 2022-12-28  16:36
 */
public enum Role {
    // 普通用户
    ORDINARY_USER("user"),
    // 管理员
    ADMIN("admin");

    private String name;

    Role(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

2.编写切面类

package com.test.boot.annotations;

import com.test.boot.aop.Auth;
import com.test.boot.enums.Role;
import com.test.boot.utils.ResultResponse;
import lombok.extern.slf4j.Slf4j;
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.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author laz
 * @date 2022-12-29 17:34
 */
@Component
@Aspect
@Slf4j
public class AuthAspect {


    /**
     * 切入点:待增强的方法
     */
    @Pointcut("@annotation(com.test.boot.aop.Auth)")
    public void AuthAround() {}

    @Around(value= "AuthAround()")
    public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {

        // 从切面织入点处通过反射机制获取织入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点所在的方法
        Method method = signature.getMethod();
        // 获取注解操作
        Auth auth = method.getAnnotation(Auth.class);
        Role[] value = auth.value();

        for (Role role : value) {
            //当注解无user角色的时候,返回错误信息,真实环境中可以通过对应用户的角色,来判断是否可以访问该接口
            if (!role.getName().equals("user")){
                return ResultResponse.fail("暂无权限!");
            }
        }

        return joinPoint.proceed();
    }

}

3.编写测试接口:

package com.test.boot.controller;

import com.test.boot.aop.Auth;
import com.test.boot.aop.MyLog;
import com.test.boot.enums.Role;
import com.test.boot.utils.ResultResponse;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: laz
 * @CreateTime: 2022-12-28  16:38
 * @Version: 1.0
 */
@RestController
@RequestMapping("test")
public class TestController {

    @RequestMapping("testAuthAnnotation/{id}")
    @MyLog(value = "测试权限注解接口")
    @Auth(Role.ORDINARY_USER)
    public ResultResponse testAuthAnnotation(@PathVariable("id")int id){
        return ResultResponse.success("id:"+id);
    }


}

4.测试

启动项目,访问该接口:

在这里插入图片描述

可以看到,接口请求成功。

更换注解值为‘ADMIN’:

在这里插入图片描述

重启项目,再次访问该接口:

在这里插入图片描述

可以看到,由于切面类设定的业务逻辑,此处无权访问!

再次提供源码地址:https://gitee.com/lianaozhe/springboot-aop

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值