通过Annotation将用户操作记录到数据库表功能实现

本文介绍了如何在SpringBoot项目中使用AOP和Fastjson实现员工管理系统中的增删改操作日志记录,包括创建操作日志表、定义数据模型、使用Log注解、AOP切面处理、JWT验证等步骤。
摘要由CSDN通过智能技术生成

一、背景

        在用户对我们所开发的系统访问的时候,需要我们的系统具有强大的健壮性,使得给与用户的体验感十足。在业务开发的过程中,我们通过将几个相关的操作绑定成一个事件,使得安全性以及数据的前后一致性得到提高。但是在溯源方面,我们往往没有很好的解决方案,我们无法得知错误的具体信息,这给后期的debug带来了一定的负担,那么我们如果将用户的操作具体信息可以记录到数据库中,那么是不是就可以溯源了?

二、任务:将一个员工管理系统中的增删改相关接口的操作日志记录到数据库中。

三、实现

3.1 我们需要插入一个数据表:log代表着插入数据的格式(数据表中不需要有任何数据):

-- 操作日志表
create table operate_log(
    id int unsigned primary key auto_increment comment 'ID',
    operate_user int unsigned comment '操作人ID',
    operate_time datetime comment '操作时间',
    class_name varchar(100) comment '操作的类名',
    method_name varchar(100) comment '操作的方法名',
    method_params varchar(1000) comment '方法参数',
    return_value varchar(2000) comment '返回值',
    cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';


除此之外,我们需要在pom文件中引入fastjson和aop的依赖:

<!--        aop依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

3.2 目录结构如下所示:

 3.3 首先我们需要定义一个操作类OperateLog

package com.bytedance.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id; //ID
    private Integer operateUser; //操作人ID
    private LocalDateTime operateTime; //操作时间
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
    private Long costTime; //操作耗时
}

3.4 其次我们需要定义一个注解文件Log

package com.bytedance.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 当前注解什么时候生效
@Target(ElementType.METHOD) // 注解有效的地方

//定义一个注解
public @interface Log {
}

3.5 接着我们定义一个LogAspect的切面,这一部分是将这些代码从主干(controller)中抽离出来,以便于复用和改动,如果我们在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。

package com.bytedance.aop;
import com.alibaba.fastjson.JSONObject;
import com.bytedance.mapper.OperateLogMapper;
import com.bytedance.pojo.OperateLog;
import com.bytedance.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Array;
import java.time.LocalDateTime;
import java.util.Arrays;

// 定义切面类
@Component
@Aspect // 这是一个切面类
@Slf4j
public class LogAspect {
    // 注入OperateLogMapper的bean对象
    @Autowired
    private OperateLogMapper operateLogMapper;
    @Autowired
    private HttpServletRequest request;
    // 定义一个通知方法
    @Around("@annotation(com.bytedance.anno.Log)") // 匹配的是方法上的加有Log注解的方法
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取操作人id - 当前登陆的员工的id 即获得请求头中的jwt令牌,解析
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUserId = (Integer) claims.get("id");
        // 操作时间
        LocalDateTime operateTime = LocalDateTime.now();
        // 操作类名
        String className = joinPoint.getTarget().getClass().getName();
        // 操作方法名
        String methodName = joinPoint.getSignature().getName();
        // 操作方法参数
//        String methodParams = joinPoint.getArgs().toString();注意不能这么写
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);
        // 方法开始时间
        long start = System.currentTimeMillis();
        // 调用原始方法运行
        Object res = joinPoint.proceed();
        // 方法返回值
        String returnValue = JSONObject.toJSONString(res);
        // 方法结束时间
        long end = System.currentTimeMillis();
        // 操作耗时
        long costTime = end - start;

        // 记录操作日志 需要调用OperateLogMapper中的insert方法,所以得注入bean对象
        OperateLog operateLog = new OperateLog(null , operateUserId,operateTime,className,methodName,methodParams,returnValue,costTime);
        operateLogMapper.insert(operateLog);
        log.info("AOP记录操作日志:{}",operateLog);
        return res;
    }
}

3.6 OperateLogMapper负责将改动的数据写入数据库中

package com.bytedance.mapper;

import com.bytedance.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperateLogMapper {

    //插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
    public void insert(OperateLog log);

}

3.7 最后将EmpController中需要记录日志的操作上打上@Log的标注即可。

package com.bytedance.controller;

import com.bytedance.anno.Log;
import com.bytedance.pojo.Emp;
import com.bytedance.pojo.PageBean;
import com.bytedance.pojo.Result;
import com.bytedance.service.DeptService;
import com.bytedance.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Delete;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.List;

@RestController
@Slf4j
@RequestMapping("/emps")
public class EmpController {

    @Autowired
    private EmpService empService;

    @GetMapping
    public Result page(@RequestParam(defaultValue = "1") Integer page ,
                       @RequestParam(defaultValue = "10") Integer pageSize,
                       String name, Short gender, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin , @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
        log.info("分页参数为:{},{},{},{},{},{}",page,pageSize,name,gender,begin,end);
        PageBean pageBean = empService.page(page,pageSize,name,gender,begin,end);
        return Result.success(pageBean);
    }

    // 批量删除员工信息
    @Log
    @DeleteMapping("/{ids}")
    public Result delete(@PathVariable List<Integer> ids){
        log.info("批量删除员工参数为:{}",ids);
        empService.delete(ids);
        return Result.success();
    }

    // 添加员工信息
//    @PostMapping
//    public Result add(@RequestBody Emp emp){
//        log.info("新增员工信息为:{emp}",emp);
//        empService.add(emp);
//        return Result.success();
//    }

    // 根据id查询员工信息
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id){
        log.info("根据ID查询员工信息:{}",id);
        Emp emp = empService.getById(id);
        return Result.success(emp);
    }


    // 修改员工信息
    @Log
    @PutMapping
    public Result update(@RequestBody Emp emp){
        log.info("前端传过来的员工信息:{}",emp);
        empService.update(emp);
        return Result.success();
    }

}

尝试一下,成功! 

注:1、记得将引入的包名换成自己的名字。

2、如果在LogAspect切面中报错,可能你没有jwt的实现类

package com.bytedance.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;

public class JwtUtils {

    private static String signKey = "itheima";
    private static Long expire = 432000000L; // 240小时候过期

    /**
     * 生成JWT令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     * @return
     */
    public static String generateJwt(Map<String, Object> claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }

    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

3、如果这里的params对象是这样的,那可能是切面中获取params的方法写的不对:

 // 操作方法参数
//        String methodParams = joinPoint.getArgs().toString();注意不能这么写
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的实现浏览记录功能的示例代码: 1. 创建数据库 首先需要在 MySQL 中创建一个来存储浏览记录,可以使用以下 SQL 语句: ```sql CREATE TABLE browsing_history ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, url VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` 2. 编写 Java Servlet 创建一个 Java Servlet 来处理用户访问页面时添加浏览记录操作。以下是示例代码: ```java import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/addHistory") public class AddHistoryServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final String DB_URL = "jdbc:mysql://localhost:3306/test"; private static final String DB_USER = "root"; private static final String DB_PASSWORD = "password"; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String url = request.getParameter("url"); String title = request.getParameter("title"); int userId = 1; // TODO: 获取当前用户 ID try { Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); String sql = "INSERT INTO browsing_history (user_id, url, title) VALUES (?, ?, ?)"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setInt(1, userId); stmt.setString(2, url); stmt.setString(3, title); stmt.executeUpdate(); conn.close(); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } response.sendRedirect(url); } } ``` 这个 Servlet 接收两个参数:`url` 和 `title`,分别用户访问的页面的 URL 和标题。在 Servlet 中,首先获取当前用户的 ID(在这个示例中,我们假设当前用户的 ID 为 1),然后将浏览记录插入到数据库中。最后,重定向到用户访问的页面。 3. 在 JSP 页面中添加链接 在 JSP 页面中,为了添加浏览记录,需要将页面中的链接都修改为使用一个特定的 URL,这个 URL 包含用户访问的原始 URL 和标题信息。以下是示例代码: ```jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>浏览记录示例</title> </head> <body> <h1>浏览记录示例</h1> <ul> <li><a href="<%=request.getContextPath()%>/addHistory?url=http://www.example.com/page1&title=页面1">页面1</a></li> <li><a href="<%=request.getContextPath()%>/addHistory?url=http://www.example.com/page2&title=页面2">页面2</a></li> <li><a href="<%=request.getContextPath()%>/addHistory?url=http://www.example.com/page3&title=页面3">页面3</a></li> </ul> </body> </html> ``` 在这个示例中,我们为每个页面添加一个链接,链接的 URL 包含了原始页面的 URL 和标题信息。当用户点击这些链接时,将会调用之前创建的 Servlet,将浏览记录插入到数据库中,并重定向到原始页面。 4. 显示浏览记录 最后,为了显示用户的浏览记录,可以创建一个 JSP 页面来查询数据库,并将结果显示出来。以下是示例代码: ```jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="java.sql.*" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>浏览记录示例</title> </head> <body> <h1>浏览记录</h1> <table> <thead> <tr> <th>时间</th> <th>标题</th> <th>URL</th> </tr> </thead> <tbody> <% try { Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); String sql = "SELECT * FROM browsing_history WHERE user_id = ? ORDER BY timestamp DESC"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setInt(1, 1); // TODO: 获取当前用户 ID ResultSet rs = stmt.executeQuery(); while (rs.next()) { String title = rs.getString("title"); String url = rs.getString("url"); Date timestamp = rs.getTimestamp("timestamp"); out.println("<tr>"); out.println("<td>" + timestamp + "</td>"); out.println("<td>" + title + "</td>"); out.println("<td><a href=\"" + url + "\">" + url + "</a></td>"); out.println("</tr>"); } conn.close(); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } %> </tbody> </table> </body> </html> ``` 这个 JSP 页面会查询数据库,获取当前用户的浏览记录,并将记录显示在一个格中。需要注意的是,这个示例中用户 ID 都是写死的,需要根据具体的业务逻辑将其替换为实际的用户 ID。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值