日志模块的实现

 

日志管理设计说明

业务设计说明

本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。其表设计语句如下:

CREATE TABLE `sys_logs` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT  NULL COMMENT '登陆用户名',
  `operation` varchar(50) DEFAULT NULL COMMENT '用户操作',
  `method` varchar(200) DEFAULT NULL COMMENT '请求方法',
  `params` varchar(5000) DEFAULT NULL COMMENT '请求参数',
  `time` bigint(20) NOT NULL COMMENT '执行时长(毫秒)',
  `ip` varchar(64) DEFAULT NULL COMMENT 'IP地址',
  `createdTime` datetime DEFAULT NULL COMMENT '日志记录时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系统日志';

完整的数据库信息在我的码云里,可以去码云里自行下载。

码云地址:https://gitee.com/Robert8640/bootstrap-3.3.7-dist.git

API设计说明

日志业务后台API分层架构及调用关系如图:

说明:分层的目的主要将复杂问题简单化,实现各司其职,各尽所能。

日志管理列表页面呈现

业务时序分析

程序编写一般分为两种:由上至下和由下至上。

由上至下:通过前端页面分析,写后端代码

由下至上:通过后端提供的URL,参数等信息写前端页面

我们这次通过由上至下的方式去写代码。

首先我们启动服务器,打开谷歌浏览器,访问http://localhost/doIndexUI,按F12打开控制台,然后点开系统管理的日志管理,这时候会发现“log_list”报404错误。我们将其打开。

可以得知日志查询的URL为:http://localhost/log/log_list

请求方式为:GET

业务时序图为:

Controller实现

业务描述与设计实现

基于日志管理的请求业务,在PageController中添加doLogUI方法,doPageUI方法分别用于返回日志列表页面,日志分页页面。

关键代码设计与实现

由时序图可以得知,客户端向服务端发送一个“log/log_list”请求,服务端向客户端传一个“log_list.html”页面;

客户端向服务端发送一个“doPageUI”请求,服务端向客户端传一个“page.html”页面。

第一步:在PageController中定义返回日志列表的方法。代码如下:

@GetMapping("log/log_list")
public String doLogUI() {
	return "sys/log_list";
}

第二步:在PageController中定义用于返回分页页面的方法。代码如下:

@RequestMapping("doPageUI")
public String doPageUI() {
	return "common/page";
}

再次访问

数据架构分析

日志查询服务端数据基本架构如图:

 

服务端API架构图及业务时序图分析

 服务端日志分页查询代码基本架构

 

服务端日志列表数据查询时序图:

服务端关键业务及代码分析

Entity类实现

业务描述及设计实现

构建实体对象(POJO)封装从数据库查询到的记录,一行记录映射为内存中一个的这样的对象。对象属性定义时尽量与表中字段有一定的映射关系,并添加对应的set/get/toString等方法,便于对数据进行更好的操作。

关键代码分析及实现

package com.sy.pj.sys.pojo;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;
/**
 * @author Robert
 */
@Data
public class SysLog implements Serializable {

    private static final long serialVersionUID = 5413565156381864339L;
    private Integer id;
    //用户名
    private String username;
    //用户操作
    private String operation;
    //请求方法
    private String method;
    //请求参数
    private String params;
    //执行时长(毫秒)
    private Long time;
    //IP地址
    private String ip;
    //创建时间
    private Date createdTime;

}

说明:通过此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据,实现层与层之间数据的传递。

Controller类实现

控制层对象主要负责请求和响应数据的处理,例如,本模块首先要通过控制层对象处理请求参数,然后通过业务层对象执行业务逻辑,再通过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。

定义控制层值对象(VO),目的是基于此对象封装控制层响应结果(在此对象中主要是为业务层执行结果添加状态信息)。Spring MVC框架在响应时可以调用相关API(例如jackson)将其对象转换为JSON格式字符串。

package com.sy.pj.common.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class JsonResult {
    /*状态码*/
    private int state=1;
    /*状态信息*/
    private String message="ok";
    /*正确数据*/
    private Object data;

    public JsonResult(int state) {
        this.state = state;
    }

    public JsonResult(String message) {
        this.message = message;
    }

    public JsonResult(Object data) {
        this.data = data;
    }
    //出现异常时调用
    public JsonResult(Throwable t){
        this.state=0;
        this.message=t.getMessage();
    }

}

由时序图分析,客户端向服务端的Controller控制层发送一个doFindPageObjects请求

package com.sy.pj.sys.controller;

import com.sy.pj.common.pojo.JsonResult;
import com.sy.pj.common.pojo.PageObject;
import com.sy.pj.sys.pojo.SysLog;
import com.sy.pj.sys.service.SysLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Robert
 */
@RequestMapping("/log/")
@RestController
public class SysLogController {
    @Autowired
    private SysLogService sysLogService;


    @GetMapping("doFindPageObjects")
    public JsonResult doFindPageObjects(String username, Integer pageCurrent){
        PageObject<SysLog> pageObject= sysLogService.findPageObjects(username, pageCurrent);
        return new JsonResult(pageObject);//封装的是正确的响应结果
    }
}

Controller层向业务层发送一个findPageObjects的请求

server接口类及实现类

业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通过业务方法中的参数接收控制层数据(例如username,pageCurrent)并校验。然后基于用户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。

定义日志业务接口及方法,暴露外界对日志业务数据的访问

package com.sy.pj.sys.service;

import com.sy.pj.common.pojo.PageObject;
import com.sy.pj.sys.pojo.SysLog;

public interface SysLogService {
    /**
     * @param name 基于条件查询时的参数名
     * @param pageCurrent 当前的页码值
     * @return 当前页记录+分页信息
     */

    PageObject<SysLog> findPageObjects(
            String username,
            Integer pageCurrent);

}

业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码参考如下:

package com.sy.pj.common.pojo;

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

import java.io.Serializable;
import java.util.List;

/**
 * @author Robert
 */
@Data
@NoArgsConstructor //无参构造

public class PageObject<T> implements Serializable {

    private static final long serialVersionUID = 8387806564138656480L;

    /**总记录数*/
    private Integer rowCount;
    /**当前页记录*/
    private List<T> records;
    /**总页数(计算出来)*/
    private Integer pageCount;
    /**当前页码值*/
    private Integer pageCurrent;
    /**页面大小(每页最多显示多少条记录)*/
    private Integer pageSize;

    public PageObject(Integer rowCount, List<T> records, Integer pageCurrent, Integer pageSize) {
        this.rowCount = rowCount;
        this.records = records;
        this.pageCurrent = pageCurrent;
        this.pageSize = pageSize;
        this.pageCount=rowCount/pageSize;
        if(this.rowCount%this.pageSize!=0)this.pageCount++;
    }

}

日志业务实现类,用于具体执行日志业务数据的分页查询操作的具体方法

package com.sy.pj.sys.service.serviceimpl;

import com.sy.pj.common.ServiceEcxeption.ServiceException;
import com.sy.pj.common.pojo.PageObject;
import com.sy.pj.sys.dao.SysLogDao;
import com.sy.pj.sys.pojo.SysLog;
import com.sy.pj.sys.service.SysLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
@Service
public class SysLogServiceImpl implements SysLogService {
    @Autowired
    private SysLogDao sysLogDao;

        /**
         * @param username    基于条件查询时的参数名
         * @param pageCurrent 当前的页码值
         * @return 当前页记录+分页信息
         */
        @Override
        public PageObject<SysLog> findPageObjects(String username, Integer pageCurrent) {
            // 1.验证参数的合法性
            // 1.1 验证pageCurrent的合法性
            // 不合法抛出IllegalArgumentException异常
            if (pageCurrent == null||pageCurrent<1) {
                throw new IllegalArgumentException("当前页码值不正确");
            }
            //2.基于条件查询总记录数
            //2.1) 执行查询
            int rowCount =sysLogDao.getRowCount(username);
            if (rowCount == 0) {
                throw new ServiceException("系统没有查到对应记录");
            }
            //3.基于条件查询当前页记录(pageSize定义为5)
            //3.1)定义pageSize
            int pageSize=5;
            //3.2)计算startIndex
            int startIndex=(pageCurrent-1)*pageSize;
            //3.3)执行当前数据的查询操作
            List<SysLog> records=
                    sysLogDao.findPageObjects(username,startIndex,pageSize);
            //4.对分页信息以及当前页记录进行封装
            //4.1)构建PageObject对象
            PageObject<SysLog> pageObject=new PageObject<>();
            //4.2)封装数据
            pageObject.setPageCount((rowCount-1)/pageSize+1);//总页数(通过计算获得)
            pageObject.setPageCurrent(pageCurrent);//当前的页码值
            pageObject.setPageSize(pageSize);//当前页面显示的记录大小
            pageObject.setRecords(records);//当前页记录
            pageObject.setRowCount(rowCount);//总行数(通过查询获得)
            //5 返回封装结果
            return pageObject;
        }
}

在该类中 ServiceException是一个自己定义的异常, 通过自定义异常可更好的实现对业务问题的描述,同时可以更好的提高用户体验。

package com.sy.pj.common.ServiceEcxeption;

public class ServiceException extends RuntimeException {
    private static final long serialVersionUID = 1661395446600784985L;

    public ServiceException() {
        super();
    }
    public ServiceException(String message) {
        super(message);
        // TODO Auto-generated constructor stub
    }
    public ServiceException(Throwable cause) {
        super(cause);
        // TODO Auto-generated constructor stub
    }
}

Dao接口实现

通过数据层对象,基于业务层参数数据查由时序图可知,syslog实现类向Dao数据层发送一个getRowCount请求,Dao数据层给syslog实现类返回一个rowcount结果

询日志记录总数以及当前页要呈现的用户行为日志信息。

定义数据层接口对象,通过将此对象保证给业务层以提供日志数据访问。

package com.sy.pj.sys.dao;

import com.sy.pj.sys.pojo.SysLog;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * @author Robert
 */
@Mapper
public interface SysLogDao {
    /**添加getRowCount方法用于按条件统计记录总数。*/
    int getRowCount(@Param("username") String username);
    /**
     * @param username  查询条件(例如查询哪个用户的日志信息)
     * @param startIndex 当前页的起始位置
     * @param pageSize 当前页的页面大小
     * @return 当前页的日志记录信息
     * 数据库中每条日志信息封装到一个SysLog对象中
     */

    List<SysLog> findPageObjects(
            @Param("username")String username,
            @Param("startIndex")Integer startIndex,
            @Param("pageSize")Integer pageSize
    );

}
  1. DAO中方法参数多余一个时尽量使用@Param注解进行修饰并指定名字,然后在Mapper文件中便可以通过类似#{username}方式进行获取,否则只能通过#{arg0}#{arg1}或者#{param1}#{param2}等方式进行获取。
  2. DAO方法中的参数应用在动态SQL中时无论多少个参数,尽量使用@Param注解进行修饰并定义。

Mapper文件实现类

业务描述及设计实现

基于Dao接口创建映射文件,在此文件中通过相关元素(例如select)描述要执行的数据操作。

关键代码设计及实现

在映射文件的设计目录(mapper/sys)中添加SysLogMapper.xml映射文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sy.pj.sys.dao.SysLogDao">
    <!--提取共性操作queryWhereId,用sql封装,之后通过id来调用,减少代码量-->
    <sql id="queryWhereId">
        from sys_logs
        <where>
            <if test="username!=null and username!=''">
                username like concat("%",#{username},"%")
            </if>
        </where>
    </sql>
   <!--添加id为getRowCount元素,按条件统计记录总数-->
   <select id="getRowCount"
            resultType="int">
        select count(*)
        <include refid="queryWhereId"></include>/*调用queryWhereId*/
   </select>


    <!--添加id为findPageObject实现分页查询-->
   <select id="findPageObjects"
            resultType="com.sy.pj.sys.pojo.SysLog">
        select *
        <include refid="queryWhereId"/>
        order by createdTime desc
        limit #{startIndex},#{pageSize}
    </select>

</mapper>

启动服务器,谷歌浏览器输入http://localhost/doIndexUI,点击系统管理的日志查询可以的得到以下页面

日志管理删除操作的实现

日志删除操作相对而言是很简单的操作,只需要根据id就可以删除对应的数据。这边值得注意的就是多条数据的删除操作,传值的方式和sql语句的优化。

首先我们按F12打开控制台,选择Network,然后选择一条数据,点击删除操作按钮,可以得到删除操作的URL,请求方式和参数(页面放不下,往下可以找到)。

数据架构分析

当用户执行日志删除操作时,客户端与服务端交互时的基本数据架构,如图所示。

删除业务时序图

客户端提交删除请求,服务端对象的工作时序分析,如图所示。

Controller层的实现 

业务描述与设计实现

在日志控制层对象中,添加用于处理日志删除请求的方法。首先在此方法中通过形参接收客户端提交的数据,然后调用业务层对象执行删除操作,最后封装执行结果,并在运行时将响应对象转换为JSON格式的字符串,响应到客户端。

关键代码实现

根据前端的URL,传递方式和传值可以写出Controller类

 @PostMapping("doDeleteObjects")
    public JsonResult doDeleteObjects(Integer... ids){
        sysLogService.deleteObjects(ids);
        return new JsonResult("删除成功!");
    }

这边值得注意的就是,因为我们要实现多条数据的删除,所以我们需要在“integer”后面加“...”,这样可以接受多条id值,并提交到业务层处理。

Service接口及实现类

业务描述与设计实现

在日志业务层定义用于执行删除业务的方法,首先通过方法参数接收控制层传递的多个记录的id,并对参数id进行校验。然后基于日志记录id执行删除业务实现。最后返回业务执行结果。

关键代码实现

首先在service接口类中添加基于多个id的删除方法

 int deleteObjects(Integer... ids);

在ServiceImpl实现类中写具体的方法

 @Override
    public int deleteObjects(Integer... ids) {
        //1 判断参数合法性
        if (ids == null || ids.length == 0) {//保证有数据传入,且这个数据不等于0
            throw new IllegalArgumentException("请选择一条日志");
        }
        //2.执行删除操作
        int rows;
        try {
            rows = sysLogDao.deleteObjects(ids);
        } catch (Throwable e) {
            e.printStackTrace();
            //发出报警信息(例如给运维人员发短信)
            throw new ServiceException("系统故障,正在恢复中...");
        }
        //4.对结果进行验证
        if (rows == 0) {
            throw new ServiceException("记录可能已经不存在");
        }
        //5.返回结果
        return rows;
    }

需要在dao层添加具体的代码对数据库进行操作

Dao层接口实现

业务描述与实现

数据层基于业务层提交的日志记录id,进行删除操作

关键代码的实现

int deleteObjects(@Param("ids")Integer… ids);

接下来要写mapper层,也就是动态sql语句对数据库进行删除操作

Mapper层实现 

业务描述与实现

在SysLogDao接口对应的映射文件中添加用于执行删除业务的delete元素,此元素内部定义具体的SQL实现。

关键代码实现

<delete id="deleteObjects">
        delete from sys_logs
        <if test="ids!=null and ids.length>0">
          where id in  
            <foreach collection="ids"
                 open="("
                 close=")"
                 separator=","
                 item="id">
                 #{id}
            </foreach>
        </if>
        <if test="ids==null or ids.length==0">
           where 1=2
        </if>
</delete>

从SQL执行性能角度分析,一般在SQL语句中不建议使用in表达式,可以参考如下代码进行实现(重点是forearch中or运算符的应用):

<delete id="deleteObjects">
        delete from sys_logs
        <choose>
            <when test="ids!=null and ids.length>0">
                <where>
                    <foreach collection="ids"
                             item="id"
                             separator="or">
                        id=#{id}
                    </foreach>
                </where>
            </when>
            <otherwise>
                where 1=2
            </otherwise>
        </choose>
    </delete>

说明:这里的choose元素也为一种选择结构,when元素相当于if,otherwise相当于else的语法。

写代码就是不断优化和改进的过程,不能只满足实现其功能上。

日志管理数据添加实现

日志数据的添加实现是该模块的最核心也是最值得研究学习的一点(对于初学者而言)。

日志的目的是为了记录哪位用户在什么地方登录,进行了什么操作的,形成一个文档,如果有人搞破坏可以找到“犯人”。

记录日志的话,首先我们会想到通过在类里面添加方法存入到数据库中。如果这样做的话,每个实现类都要添加这样的方法做记录,代码显得臃肿不易读。如果以后每添加一个对象或方法都要修改代码,不易于维护。那么有什么方法可以解决这个问题呢?这时候我就要说下AOP了。

  AOP是一种设计思想,是面向切面的编程,他是面向对象的编程(oop)的一种补充和完善。在预编译和运行期动态代理方式,实现给程序动态统一添加额外功能的一种技术。通常将面向对象理解为一个静态过程,面向切面的运行期代理方式,可以理解为一个动态过程,可以在对象运行时动态织入一些扩展功能... ...

具体内容可以看看我另一篇博客:https://blog.csdn.net/weixin_49792497/article/details/109374014

Dao接口实现

业务描述与设计实现

数据层基于业务层的持久化请求,将业务层提交的用户行为日志信息写入到数据库。

关键代码设计与实现

在SysLogDao接口中添加用于实现日志信息持久化的方法。

int insertObject(SysLog entity);

Mapper映射文件

业务描述与设计实现

基于SysLogDao中方法的定义,编写用于数据持久化的SQL元素。

关键代码设计与实现

在SysLogMapper.xml中添加insertObject元素,用于向日志表写入用户行为日志。

<insert id="insertObject">
       insert into sys_logs
       (username,operation,method,params,time,ip,createdTime)
       values
(#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
</insert>

Service接口及实现类

业务描述与设计实现

将日志切面中抓取到的用户行为日志信息,通过业务层对象方法持久化到数据库。

关键代码实现

先在SysLogService接口中,添加保存日志信息的方法。

void saveObject(SysLog entity)

然后在在SysLogServiceImpl类中添加具体的保存日志信息的方法。

@Override
	public void saveObject(SysLog entity) {
	  sysLogDao.insertObject(entity);
}

日志切面Aspect实现

业务描述与设计实现

在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业务层对象对日志日志信息做进一步处理,将其存入数据库中。

关键代码设计与实现

在springboot工程中要想使用aop时,应在pom文件中添加依赖:

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

 我们首先在common包下新建一个包,包名为:aspect,我们将切面对象放在这里面

在创建一个包叫注解,报名为:annotation,

我们先在aspect包下,新建一个切面SysLogAspect,在切面上加一个注解@component,表示这是一个普通的组件对象:再加一个@aspect表示这还是一个aop中切面对象。在切面对象中定义(1)切入点(Pointcut):织入扩展功能的一些连接点的集合 (2)通知方法(Advice):封装了扩展逻辑的方法。

我们先在切面对象有了,那么我们怎么定义切入点和通知方法呢?

切入点定义有很多方式,我们先用注解的方式来实现,我们在日志的实现类上加一个注解@RequiredLog。使用这个注解描述的方法,就是一个切入点方法。由于这个注解没有,所以需要我们自己定义这个注解。

我们在刚刚新建的annotation的包下新建一个Annotation类,类名为RequiredLog。

这个类是我们自定义的一个注解,现在就考到了我们java基础部分的知识点了。那就是当我们自定义一个注解时一定要注意哪几个方面?

首先,我们要定义这个注解可以描述什么,其次就是这个注解什么时候起作用。

RequiredLog注解代码如下

@Retention(RetentionPolicy.RUNTIME)//定义我们的注解何时有效
@Target(ElementType.METHOD) //定义我们的注解可以修饰谁
public @interface RequiredLog {
    
    
}

 这时我们在日志的实现类上加上这个注解就表示,在这个方法运行时要加入一个日志功能。这时我们要在切面对象中定义@RequiredLog这个注解是一个切入点

这样才可以真正实现在日志实现类的方法运行时加入日志功能。

所以我们现在要在切面对象中定义一个切入点

package com.sy.pj.common.aspect;


import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SysLogAspect {
    //通过Pointcut定义一个切入点,@annotation方式为定义切入点的一种方式,
    //在这里表示业务对象中由com.cy.pj.common.annotation.RequiredLog注解描述的方法为一些切入点方法
    @Pointcut("@annotation(com.sy.pj.common.annotation.RequiredLog)")
    public void doLog(){}//doLog方法仅仅是@Pointcut注解的一个载体,方法体内不需要写任何内容

    /**
     * @Around 注解描述的方法可以在目标方法执行之前和之后做功能扩展
     * @param joinPoint 封装了目标方法信息的一个对象(连接点对象)
     * @return 目标方法的执行结果
     * @throws Throwable
     */
    
}

然后在我们的实现类上面加上我们刚刚自定义的注解,为了提高日志的可读性,我们应该知道所切入点的方法具体叫什么,可以再注解后面加上“日志查询”

 @RequiredLog("查询日志")
        @Override
        public PageObject<SysLog> findPageObjects(String username, Integer pageCurrent) {

 当然我们也可以在实现类中的删除方法上面加这个注解,然后在后面加上“删除日志”,提高日志记录的可读性。我们先以查询日志做示范。

如果我们要在注解后面加字符串的话,就要稍微修改下我们的注解:

package com.sy.pj.common.annotation;

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 RequiredLog {

    String value() default "";
}

在我们写用作日志记录的切面类之前,我们应该看下日志的实体类,需要哪些值:

由于我们还没有做登录模块,所以username和ip暂时用一个固定值代替

这里面比较难获取到的就是用户操作,也就是我们注解后面的“日志查询”。

如果想获取注解里面的东西,我们首先要获取注解

如果要获取注解的话,前提是要获取到目标方法(加注解的方法)。

一个类里面可能有很多相同名字的方法,所以只获取方法的名字是远远不够的,方法的唯一标识是:方法名和参数列表

所以我们还要获得参数列表。

如果要想获得一个类里面的一个方法,那我们就要先获取到这个类。

到这里就变得简单了。

具体代码如下:

package com.sy.pj.common.aspect;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.sy.pj.common.annotation.RequiredLog;
import com.sy.pj.sys.pojo.SysLog;
import com.sy.pj.sys.service.SysLogService;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;

@Aspect
@Component
public class SysLogAspect {


    //通过Pointcut定义一个切入点,@annotation方式为定义切入点的一种方式,
    //在这里表示业务对象中由com.cy.pj.common.annotation.RequiredLog注解描述的方法为一些切入点方法
    @Pointcut("@annotation(com.sy.pj.common.annotation.RequiredLog)")
    public void doLog() {
    }//doLog方法仅仅是@Pointcut注解的一个载体,方法体内不需要写任何内容

    /**
     * @param joinPoint 封装了目标方法信息的一个对象(连接点对象)
     * @return 目标方法的执行结果
     * @throws Throwable
     * @Around 注解描述的方法可以在目标方法执行之前和之后做功能扩展
     */

    @Around("doLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long t1 = System.currentTimeMillis();
        Object result = joinPoint.proceed();//去调用目标方法,其返回值为目标方法返回值
        long t2 = System.currentTimeMillis();
        System.out.println("time:" + (t2 - t1));
        //将正常的用户行为日志写入到数据库
        saveSysLog(joinPoint, (t2 - t1));
        return result;
    }


    @Autowired
    private SysLogService sysLogService;
    private void saveSysLog(ProceedingJoinPoint joinPoint,long time) throws NoSuchMethodException, JsonProcessingException {
        //1 获取用户行为日志
        //获取目标对象类型
        Class<?> targetClass = joinPoint.getTarget().getClass();
        //获取目标方法的签名信息
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        //获取目标方法(方法的唯一标识是:方法名+参数列表)
        Method targetMethod =
                targetClass.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
        //获得RequiredLog
        RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);
        //获取操作名
        String operation = requiredLog.value();
        //封装日志信息
        SysLog entity = new SysLog();
        entity.setUsername("user");//将来这个位置是登录的用户名,我们先随便写一个代替
        entity.setIp("192.168.126.129");//先写个假的IP代替
        entity.setOperation(operation);//操作名
        entity.setMethod(targetClass.getName() + "." + targetMethod.getName());//调用方法时传递实践参数
        entity.setParams(Arrays.toString(joinPoint.getArgs()));
        entity.setTime(time);//操作的时间
        entity.setCreatedTime(new Date());//操作的日期
        //3保存用户行为
        sysLogService.saveObject(entity);
    }
}

日志模块到这里基本就结束了。这里面的用户名和IP地址,等到做完用户登录之后再做更改。

我们打开服务器,进入http://localhost/doIndexUI进行检验。

我们也可以在实现类中删除方法上加

@RequiredLog("删除日志")

 功能基本实现(日志模块的作用就是记录用户真实的行为,所以不用加修改操作)。

日志模块到这里就结束了。我们先做个总结。

总结

  • 日志管理整体业务分析与实现。

  1. 分层架构(应用层MVC:基于spring的mvc模块)。
  2. API架构(SysLogDao,SysLogService,SysLogController)。
  3. 业务架构(查询,删除,添加用户行为日志)。
  4. 数据架构(SysLog,PageObject,JsonResult,..)。
  • 日志管理持久层映射文件中SQL元素的定义及编写。

  1. 定义在映射文件”mapper/sys/SysLogMapper.xml”(必须在加载范围内)。
  2. 每个SQL元素必须提供一个唯一ID,对于select必须指定结果映射(resultType)
  3. 系统底层运行时会将每个SQL元素的对象封装一个值对象(MappedStatement)。
  • 日志管理模块数据查询操作中的数据封装。

  1. 数据层(数据逻辑)的SysLog对象应用(一行记录一个log对象)。
  2. 业务层(业务逻辑)PageObject对象应用(封装每页记录以及对应的分页信息)。
  3. 控制层(控制逻辑)的JsonResult对象应用(对业务数据添加状态信息)。
  • 日志管理控制层请求数据映射,响应数据的封装及转换(转换为json 串)。

  1. 请求路径映射,请求方式映射(GET,POST),请求参数映射(直接量,POJO)。
  2. 响应数据两种(页面,JSON串)。
  • 日志管理模块异常处理如何实现的。

  1. 请求处理层(控制层)定义统一(全局)异常处理类。
  2. 使用注解@RestControllerAdvice描述类,使用@ExceptionHandler描述方法。
  3. 异常处理规则:能处理则处理,不能处理则抛出。

 

  • 6
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值