前言

  我是在做某培训机构的外卖项目自己新增的一个功能,上网查了很多资料,资料很丰富但有些东西没有解释清楚,于是我花了一个晚上把那些大佬代码里面没有解释清楚的地方加了很多注解。仅供初学者参考。

准备工作

0. 建议了解一下aop的一些知识,以下代码是基于注解进行aop开发(你也可以基于xml进行开发原理一样)

1. 导入aop的坐标

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

2. 准备记录日志的实体类

因为我是将日志存入数据库,所以需要实体类

import lombok.Data;

/**
 * 日志记录
 */
@Data
public class Logs {
    private static final long serialVersionUID = 1L;

    private Long id;

    //修改数据库用户的项目
    private String username;

    //修改的内容
    private String details;

    //修改时间
    private String date;
}

3. 自定义一个注解用于作为aop的切点

切点(PointCut): 可以插入增强处理的连接点

import java.lang.annotation.*;

/**
 * 系统日志注解
 */
@Target({ElementType.METHOD}) //Type代表该注解可以放在类上,METHOD代表可以放在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoLog {
    /**
     * 日志内容
     * @return
     */
    String value() default "";

    /**
     * 操作日志类型
     *
     * @return (1查询,2添加,3修改,4删除)
     */
    int operateType() default 0;//这个参数用于自己传参,便于aop识别进行的操作是什么

}

解释一下上面的几个注解是什么意思:

@Target:用于表示我们这个自定义注解可以作用于类上还是方法上,有括号内的参数决定,可以传入多个参数

        ElementType.METHOD        作用于方法上

        ElementType.TYPE              作用于类上

@Retention:表示注解在程序的什么阶段生效,有括号内的参数决定,上面的是在运行时生效

@Documented:这个Annotation可以被写入javadoc

@interface 表示这是一个注解类, 不是interface,是注解类 定义注解用的,是jdk1.5之后加入的,java没有给它新的关键字,所以就用@interface 这么个东西表示了

4. 设计被增强的目标方法

@AutoLog(operateType = 1)//这个注解相当于切点,会被aop识别,参数1代表该方法进行的查询操作
    @GetMapping("/page")
    public Result<Page> page(int page, int pageSize, Long number, String beginTime, String endTime) {
        Page<Orders> pageInfo = new Page<>(page, pageSize);
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(number != null, Orders::getNumber, number)
                    .between(beginTime != null && endTime != null, Orders::getOrderTime, beginTime, endTime);
        ordersService.page(pageInfo,queryWrapper);
        return Result.success(pageInfo);
    }

我这里使用的是一个分页功能,基于MyBatisPlus实现的

5. 设计切面(重点)

先放上代码

/**
 * 日志记录
 */
@Component
@Aspect
public class AopLog {
    @Autowired
    private LogService logService;

    @Autowired
    private EmployeeService employeeService;

    //日志内容
    private String username;//操作用户名
    private String detail;//操作内容
    private String date;//操作时间


    @Pointcut("@annotation(cn.nicst.reggie.annotation.AutoLog)")
    public void logPointcut(){
    }

    /**
     * 环绕通知,用于获取sql执行时长
     * @return
     */
    @Around(value = "logPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取开始时间
        long beginTime = System.currentTimeMillis();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        date = sdf.format(beginTime);
        //获取操作的用户的用户名
        Long id = BaseContext.getCurrentId();   //获取到用户的id
        Employee employee = employeeService.getById(id);
        username = employee.getUsername();
        //执行方法
        Object result = joinPoint.proceed();
        //执行时长
        long time = System.currentTimeMillis() - beginTime;
        //获取方法的详细信息
        int i = 0;
        Class< ? > targetClass = null;
        String targetClassName = joinPoint.getTarget().getClass().getName();//动态获取目标类的名称
        String targetMethodName = joinPoint.getSignature().getName();//动态获取目标类的名称
        try {
            targetClass = Class.forName(targetClassName);//获取目标方法的字节码文件
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        Method[] methods = targetClass.getMethods();
        for (Method method : methods) {
            if(method.getName().equals(targetMethodName)){
              i = method.getAnnotation(AutoLog.class).operateType();//获取注解内的属性
            }
        }
        switch (i){
            case 1:
                detail = "进行了查询操作";
                break;
            case 2:
                detail = "添加了一条数据";
                break;
            case 3:
                detail = "修改了一条数据";
                break;
            case 4:
                detail = "删除了一条数据";
                break;
        }
        //保存日志
        saveLog(username,detail,date);
        return result;
    }



    /**
     * 记录日志,保存到数据库
     */
    public void saveLog(String username,String detail,String date){
        Logs logs = new Logs();
        logs.setUsername(username);
        logs.setDetails(detail);
        logs.setDate(date);

        boolean flag = logService.save(logs);
    }
}

几个点:

1.  Long id = BaseContext.getCurrentId(); //获取到用户的id

我这里是使用了ThreadLocal,在登录的时候将用户的id存入ThreadLocal内的,代码如下:

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

在过滤器中存入用户的id,代码如下:

//4-1 判断员工登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee")!=null){
//            log.info("用户的id为:{}",request.getSession().getAttribute("employee"));
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);
            filterChain.doFilter(request,response);
            return;
        }

        //4-2 判断用户登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("userId")!=null){
            Long userId = (Long) request.getSession().getAttribute("userId");

            BaseContext.setCurrentId(userId);
            filterChain.doFilter(request,response);
            return;
        }

2. 关于形参ProceedingJoinPoint joinPoint,我查到的资料如下:

JoinPoint 对象:

JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象. 
常用api:

方法名

功能

Signature getSignature();

获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息

Object[] getArgs();

获取传入目标方法的参数对象

Object getTarget();

获取被代理的对象

Object getThis();

获取代理对象

ProceedingJoinPoint对象

ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中, 
添加了 
Object proceed() throws Throwable //执行目标方法 
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法 两个方法.

资料来源:(47条消息) JoinPoint的用法_斜阳雨陌的博客_joinpoint

我这里把日志内容写死了,其实detail部分的日志内容还可以更加详细,比如查询了哪个表,修改了 哪个表。

有不正确的地方还请大佬指正!!!