一. 概述
1.1 业务设计说明
本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。
1.2 API设计图
日志业务后台API分层架构及调用关系如图所示
二. 列表页面呈现
2.1 业务时序分析
当点击首页左侧的"日志管理"菜单时,其总体时序分析如图所示
2.2 服务端实现
2.2.1 Controller实现
1)在PageController中添加返回日志列表的方法doLogUI
@RequestMapping("log/log_list")
public String doLogUI() {
return "sys/log_list";
}
2)在PageController中添加返回分页页面的方法doPageUI
@RequestMapping("doPageUI")
public String doPageUI() {
return "common/page";
}
2.3 客户端实现
1)在 starter.html 页面中添加 js 代码,页面加载完成以后,注册日志管理菜单项的点击事件,当点击日志管理时,执行事件处理函数,异步加载日志列表页面 log_list.html
$(function(){
doLoadUI("load-log-id","log/log_list")
})
function doLoadUI(id,url){
$("#"+id).click(function(){
$("#mainContentId").load(url);
});
}
2)在 log_list.html 页面中添加 js 代码,面加载完成以后异步加载分页页面 page.html
$(function(){
$("#pageId").load("doPageUI");
});
三. 列表数据呈现
3.1 数据架构分析
日志查询服务端数据基本架构,如图所示
3.2 API架构分析
服务端日志分页查询代码基本架构,如图所示
3.3 业务时序图分析
服务端日志列表数据查询时序图,如图所示
3.4 服务端实现
3.4.1 Entity实现
构建实体对象(POJO)封装从数据库查询到的记录,添加对应的set/get/toString等方法。此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据,实现层与层之间数据的传递。
1)定义实体类SysLog
package com.cy.pj.sys.entity;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
@Data
public class SysLog implements Serializable{
private static final long serialVersionUID = 8924387722922123121L;
private Integer id; // 主键id
private String username; // 用户名
private String operation; // 用户操作
private String method; // 请求方法
private String params; // 请求参数
private Long time; // 执行时长(毫秒)
private String ip; // IP地址
private Date createdTime; // 创建时间
}
3.4.2 Dao实现
通过数据层对象,基于业务层参数数据查询日志记录总数以及当前页要呈现的用户行为日志信息。并基于Dao接口创建映射文件,在此文件中通过相关元素(例如select)描述要执行的数据操作。
1)定义数据层接口SysLogDao
package com.cy.pj.sys.service;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysLogDao {
}
2)在SysLogDao接口中添加getRowCount方法实现按条件统计记录总数
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @return 总记录数(基于这个结果可以计算总页数)
*/
int getRowCount(@Param("username") String username);
3)在SysLogDao接口中添加findPageObjects方法实现当前页记录的数据查询操作
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @param startIndex 当前页的起始位置
* @param pageSize 当前页的页面大小
* @return 当前页的日志记录信息
* 数据库中每条日志信息封装到一个SysLog对象中
*/
List<SysLog> findPageObjects(
@Param("username")String username,
@Param("startIndex")Integer startIndex,
@Param("pageSize")Integer pageSize);
注意:
Dao中使用@Param注解参数的两种情况:
1、方法中包含多个参数时,使用@Param注解指定其名字,在Mapper文件中通过#{}获取,若不使用@Param注解描述,只能通过#{arg0},#{arg1}或者#{param1},#{param2}等方式进行获取
2、方法中参数用于动态SQL时,使用@Param修饰
4)定义映射文件SysLogMapper.xml(src/main/resources/mapper/sys)
<?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.cy.pj.sys.dao.SysLogDao">
</mapper>
5)在 标签体中添加共性sql实现
<sql id="queryWhereId">
from sys_Logs
<where>
<if test="username!=null and username!=''">
username like concat("%",#{username},"%")
</if>
</where>
</sql>
6)在 标签体中添加getRowCount实现
<select id="getRowCount" resultType="int">
select count(*)
<include refid="queryWhereId" />
</select>
7)在 标签体中添加findPageObjects实现
<select id="findPageObjects"
resultType="com.cy.pj.sys.entity.SysLog">
select *
<include refid="queryWhereId" />
order by createdTime desc
limit #{startIndex},#{pageSize}
</select>
8)编写单元测试类SysLogDaoTests
package com.cy.pj.sys.dao;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.cy.pj.sys.entity.SysLog;
@SpringBootTest
public class SysLogDaoTests {
@Autowired
private SysLogDao sysLogDao;
@Test
public void testGetRowCount() {
int rows=sysLogDao.getRowCount("admin");
System.out.println("rows="+rows);
}
@Test
public void testFindPageObjects() {
List<SysLog> list=
sysLogDao.findPageObjects("admin", 0, 3);
for(SysLog log:list) {
System.out.println(log);
}
}
3.4.3 Service实现
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通过业务方法中的参数接收控制层数据(例如username,pageCurrent)并校验。然后基于用户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。
1)定义业务对象,封装数据层返回的数据以及计算的分页信息
package com.cy.pj.common.vo;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class PageObject<T> implements Serializable{
private static final long serialVersionUID = -6309355831851522938L;
private Integer pageCurrent; // 当前页的页码值
private Integer pageSize; // 页面大小
private Integer rowCount; // 总行数(通过查询获得)
private Integer pageCount; // 总页数(通过计算获得)
private List<T> records; // 当前页记录
public PageObject(List<T> records, Integer rowCount,Integer pageSize, Integer pageCurrent) {
this.records = records;
this.rowCount = rowCount;
this.pageSize = pageSize;
this.pageCurrent = pageCurrent;
this.pageCount=(rowCount-1)/pageSize+1; // 计算分析
}
}
2)定义业务层接口SysLogService
package com.cy.pj.sys.service;
import com.cy.pj.common.vo.PageObject;
import com.cy.pj.sys.entity.SysLog;
public interface SysLogService {
/**
* @param name 基于条件查询时的参数名
* @param pageCurrent 当前的页码值
* @return 当前页记录+分页信息
*/
PageObject<SysLog> findPageObjects(
String username,
Integer pageCurrent);
}
3)定义业务异常类ServiceException
package com.cy.pj.common.exception;
public class ServiceException extends RuntimeException{
private static final long serialVersionUID = -5598865415547474216L;
public ServiceException() {
super();
}
public ServiceException(String message) {
super(message);
}
public ServiceException(Throwable cause) {
super(cause);
}
}
4)定义业务层接口实现类SysLogServiceImpl
package com.cy.pj.sys.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cy.pj.common.exception.ServiceException;
import com.cy.pj.common.vo.PageObject;
import com.cy.pj.sys.dao.SysLogDao;
import com.cy.pj.sys.entity.SysLog;
import com.cy.pj.sys.service.SysLogService;
@Service
public class SysLogServiceImpl implements SysLogService{
@Autowired
private SysLogDao sysLogDao;
@Override
public PageObject<SysLog> findPageObjects(
String name, Integer pageCurrent) {
//1.验证参数合法性
//1.1验证pageCurrent的合法性,
//不合法抛出IllegalArgumentException异常
if(pageCurrent==null||pageCurrent<1)
throw new IllegalArgumentException("当前页码不正确");
//2.基于条件查询总记录数
//2.1) 执行查询
int rowCount=sysLogDao.getRowCount(name);
//2.2) 验证查询结果,假如结果为0不再执行如下操作
if(rowCount==0)
throw new ServiceException("系统没有查到对应记录");
//3.基于条件查询当前页记录(pageSize定义为2)
//3.1)定义pageSize
int pageSize=2;
//3.2)计算startIndex
int startIndex=(pageCurrent-1)*pageSize;
//3.3)执行当前数据的查询操作
List<SysLog> records=
sysLogDao.findPageObjects(name, startIndex, pageSize);
//4.对分页信息以及当前页记录进行封装
//4.1)构建PageObject对象
PageObject<SysLog> pageObject=new PageObject<>();
//4.2)封装数据
pageObject.setPageCurrent(pageCurrent);
pageObject.setPageSize(pageSize);
pageObject.setRowCount(rowCount);
pageObject.setRecords(records);
pageObject.setPageCount((rowCount-1)/pageSize+1);
//5.返回封装结果。
return pageObject;
}
}
3.4.4 Controller实现
控制层对象主要负责请求和响应数据的处理,例如,本模块首先要通过控制层对象处理请求参数,然后通过业务层对象执行业务逻辑,再通过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。
1)定义控制层值对象JsonResult,封装控制层响应结果(在此对象中主要是为业务层执行结果添加状态信息)。
package com.cy.pj.common.vo;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class JsonResult implements Serializable {
private static final long serialVersionUID = 4782243556164735818L;
private int state; // 状态码, 1表示SUCCESS,0表示ERROR
private String message; // 状态信息
private Object data; // 正确数据
// 基于此构造方法进行消息提示的初始化
public JsonResult(String message){
this.state=1;
this.message=message;
}
// 基于此构造方法进行正确数据的初始化
public JsonResult(Object data){
this.state=1;
this.data=data;
}
// 基于此构造方法进行错误信息的初始化
public JsonResult(Throwable e){// Throwable是所有异常类的父类
this.state=0;//error
this.message=e.getMessage();// 获取异常信息
}
}
2)定义控制层对象SysLogController
package com.cy.pj.sys.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cy.pj.common.vo.JsonResult;
import com.cy.pj.common.vo.PageObject;
import com.cy.pj.sys.entity.SysLog;
import com.cy.pj.sys.service.SysLogService;
@Controller
@RequestMapping("/log/")
public class SysLogController {
@Autowired
private SysLogService sysLogService;
@RequestMapping("doFindPageObjects")
@ResponseBody
public JsonResult doFindPageObjects(String username,Integer pageCurrent){
PageObject<SysLog> pageObject= sysLogService.findPageObjects(username,pageCurrent);
return new JsonResult(pageObject);
}
}
3)定义全局异常处理类GlobalExceptionHandler
package com.cy.pj.common.web;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cy.pj.common.vo.JsonResult;
@ControllerAdvice
public class GlobalExceptionHandler {
// JDK中的自带的日志API
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public JsonResult doHandleRuntimeException(
RuntimeException e){
e.printStackTrace(); // 也可以写日志异常信息
return new JsonResult(e); // 封装
}
}
控制层响应数据处理分析,如图所示
3.5 客户端实现
3.5.1 页面事件分析
当用户点击首页日志管理时,其页面流转分析如图所示
3.5.2 日志列表信息呈现
日志分页页面加载完成以后,向服务端发起异步请求加载日志信息,当日志信息加载完成需要将日志信息、分页信息呈现到列表页面上。
1)log_list.html页面加载完成,添加js代码,向服务端发起异步请求
$(function(){
$("#pageId").load("doPageUI",function(){
doGetObjects();
});
});
2)定义异步请求处理函数doGetObjects
function doGetObjects(){
//debugger;//断点调试
//1.定义url和参数
var url="log/doFindPageObjects"
var params={"pageCurrent":1};//pageCurrent=2
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?//可以,必须符合标识符的规范
$.getJSON(url,params,function(result){
//请问result是一个字符串还是json格式的js对象?对象
doHandleQueryResponseResult(result);
});//特殊的ajax函数
}
result 结果对象分析,如图所示
3)定义回调函数,处理服务端的响应结果
function doHandleQueryResponseResult(result) { //JsonResult
if (result.state == 1) {//ok
//更新table中tbody内部的数据
doSetTableBodyRows(result.data.records);//将数据呈现在页面上
//更新页面page.html分页数据
//doSetPagination(result.data); //此方法写到page.html中
} else {
alert(result.message);
}
}
4)将异步响应结果呈现在table的tbody位置
function doSetTableBodyRows(records) {
//1.获取tbody对象,并清空对象
var tBody = $("#tbodyId");
tBody.empty();
//2.迭代records记录,并将其内容追加到tbody
for (var i in records) {
//2.1 构建tr对象
var tr = $("<tr></tr>");
//2.2 构建tds对象
var tds = doCreateTds(records[i]);
//2.3 将tds追加到tr中
tr.append(tds);
//2.4 将tr追加到tbody中
tBody.append(tr);
}
}
5)创建每行中的td元素,并填充具体业务数据
function doCreateTds(data) {
var tds = "<td><input type='checkbox' class='cBox' name='cItem' value='" + data.id + "'></td>" +
"<td>" + data.username + "</td>" +
"<td>" + data.operation + "</td>" +
"<td>" + data.method + "</td>" +
"<td>" + data.params + "</td>" +
"<td>" + data.ip + "</td>" +
"<td>" + data.time + "</td>";
return tds;
}
3.5.3 分页数据信息呈现
日志信息列表初始化完成以后初始化分页数据(调用setPagination函数),然后再点击上一页,下一页等操作时,更新页码值,执行基于当前页码值的查询。
1)在page.html页面中定义doSetPagination方法,实现分页数据初始化
function doSetPagination(page) {
//1.始化数据
$(".rowCount").html("总记录数(" + page.rowCount + ")");
$(".pageCount").html("总页数(" + page.pageCount + ")");
$(".pageCurrent").html("当前页(" + page.pageCurrent + ")");
//2.绑定数据(为后续对此数据的使用提供服务)
$("#pageId").data("pageCurrent", page.pageCurrent);
$("#pageId").data("pageCount", page.pageCount);
}
2)分页页面page.html中注册点击事件
$(function () {
//事件注册
$("#pageId").on("click", ".first,.pre,.next,.last", doJumpToPage);
})
3)定义doJumpToPage方法(通过此方法实现当前数据查询)
function doJumpToPage() {
//1.获取点击对象的class值
var cls = $(this).prop("class");//Property
//2.基于点击的对象执行pageCurrent值的修改
//2.1获取pageCurrent,pageCount的当前值
var pageCurrent = $("#pageId").data("pageCurrent");
var pageCount = $("#pageId").data("pageCount");
//2.2修改pageCurrent的值
if (cls == "first") {//首页
pageCurrent = 1;
} else if (cls == "pre" && pageCurrent > 1) {//上一页
pageCurrent--;
} else if (cls == "next" && pageCurrent < pageCount) {//下一页
pageCurrent++;
} else if (cls == "last") {//最后一页
pageCurrent = pageCount;
} else {
return;
}
//3.对pageCurrent值进行重新绑定
$("#pageId").data("pageCurrent", pageCurrent);
//4.基于新的pageCurrent的值进行当前页数据查询
doGetObjects();
}
4)修改分页查询方法doGetObjects
function doGetObjects() {
//debugger;//断点调试
//1.定义url和参数
var url = "log/doFindPageObjects"
//? 请问data函数的含义是什么?(从指定元素上获取绑定的数据)
//此数据会在何时进行绑定?(setPagination,doQueryObjects)
var pageCurrent = $("#pageId").data("pageCurrent");
//为什么要执行如下语句的判定,然后初始化pageCurrent的值为1
//pageCurrent参数在没有赋值的情况下,默认初始值应该为1.
if (!pageCurrent) pageCurrent = 1;
var params = { "pageCurrent": pageCurrent };//pageCurrent=2
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?可以,必须符合标识符的规范
$.getJSON(url, params, function (result) {
//请问result是一个字符串还是json格式的js对象?对象
doHandleQueryResponseResult(result);
}
);//特殊的ajax函数 }
3.5.4 列表页面信息查询实现
当用户点击日志列表的查询按钮时,基于用户输入的用户名进行有条件的分页查询,并将查询结果呈现在页面。
1)log_list.html页面加载完成,在查询按钮上进行事件注册
$(".input-group-btn").on("click",".btn-search",doQueryObjects)
2)定义查询按钮对应的点击事件处理函数doQueryObjects
function doQueryObjects() {
//为什么要在此位置初始化pageCurrent的值为1?
//数据查询时页码的初始位置也应该是第一页
$("#pageId").data("pageCurrent", 1);
//为什么要调用doGetObjects函数?
//重用js代码,简化jS代码编写。
doGetObjects();
}
3)修改分页查询函数doGetObjects
function doGetObjects() {
//debugger;//断点调试
//1.定义url和参数
var url = "log/doFindPageObjects"
//? 请问data函数的含义是什么?(从指定元素上获取绑定的数据)
//此数据会在何时进行绑定?(setPagination,doQueryObjects)
var pageCurrent = $("#pageId").data("pageCurrent");
//为什么要执行如下语句的判定,然后初始化pageCurrent的值为1
//pageCurrent参数在没有赋值的情况下,默认初始值应该为1.
if (!pageCurrent) pageCurrent = 1;
var params = { "pageCurrent": pageCurrent };
//为什么此位置要获取查询参数的值?
//一种冗余的应用方法,目的时让此函数在查询时可以重用。
var username = $("#searchNameId").val();
//如下语句的含义是什么?动态在json格式的js对象中添加key/value,
if (username) params.username = username;//查询时需要
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?可以,必须符合标识符的规范
$.getJSON(url, params, function (result) {
//请问result是一个字符串还是json格式的js对象?对象
doHandleQueryResponseResult(result);
}
);
}
四. 删除操作实现
4.1 数据架构分析
当用户执行日志删除操作时,客户端与服务端交互时的基本数据架构
4.2 业务时序分析
客户端提交删除请求,服务端对象的工作时序分析,如图所示
4.3 服务端实现
数据层基于业务层提交的日志记录id,进行日志删除操作
4.3.1 Dao实现
1)在SysLogDao接口中添加方法
int deleteObjects(@Param("ids")Integer… ids);
2)在 SysLogMapper.xml 中添加SQL映射
<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>
4.3.2 Service实现
1)在SysLogService接口中,添加基于多个id进行日志删除的方法 deleteObjects
int deleteObjects(Integer… ids)
2)在SysLogServiceImpl实现类中添加删除业务的具体实现
@Override
public int deleteObjects(Integer… ids) {
//1.判定参数合法性
if(ids==null||ids.length==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;
}
4.3.3 Controller实现
1)在SysLogController中添加用于执行删除业务的方法
@RequestMapping("doDeleteObjects")
@ResponseBody
public JsonResult doDeleteObjects(Integer… ids){
sysLogService.deleteObjects(ids);
return new JsonResult("delete ok");
}
2)启动tomcat进行访问测试,打开浏览器输入如下网址
http://localhost/log/doDeleteObjects?ids=1,2,3
4.4 客户端实现
用户在页面上首先选择要删除的元素,然后点击删除按钮,将用户选择的记录id异步提交到服务端,最后在服务端执行日志的删除动作
1)页面加载完成以后,在删除按钮上进行点击事件注册
$(".input-group-btn")
.on("click", ".btn-delete", doDeleteObjects)
2)定义删除操作对应的事件处理函数
function doDeleteObjects() {
//1.获取选中的id值
var ids = doGetCheckedIds();
if (ids.length == 0) {
alert("至少选择一个");
return;
}
//2.发异步请求执行删除操作
var url = "log/doDeleteObjects";
var params = { "ids": ids.toString() };
console.log(params);
$.post(url, params, function (result) {
if (result.state == 1) {
alert(result.message);
doGetObjects();
} else {
alert(result.message);
}
});
}
3)定义获取用户选中的记录id的函数
function doGetCheckedIds() {
//定义一个数组,用于存储选中的checkbox的id值
var array = [];//new Array();
//获取tbody中所有类型为checkbox的input元素
$("#tbodyId input[type=checkbox]").
//迭代这些元素,每发现一个元素都会执行如下回调函数
each(function () {
//假如此元素的checked属性的值为true
if ($(this).prop("checked")) {
//调用数组对象的push方法将选中对象的值存储到数组
array.push($(this).val());
}
});
return array;
}
4)Thead中全选元素的状态影响tbody中checkbox对象状态
function doChangeTBodyCheckBoxState() {
//1.获取当前点击对象的checked属性的值
var flag = $(this).prop("checked");//true or false
//2.将tbody中所有checkbox元素的值都修改为flag对应的值。
//第一种方案
/* $("#tbodyId input[name='cItem']")
.each(function(){
$(this).prop("checked",flag);
}); */
//第二种方案
$("#tbodyId input[type='checkbox']")
.prop("checked", flag);
}
5)Tbody中checkbox的状态影响thead中全选元素的状态
function doChangeTHeadCheckBoxState() {
//1.设定默认状态值
var flag = true;
//2.迭代所有tbody中的checkbox值并进行与操作
$("#tbodyId input[type='checkbox']")
.each(function () {
flag = flag & $(this).prop("checked")
});
//3.修改全选元素checkbox的值为flag
$("#checkAll").prop("checked", flag);
}
6)完善业务刷新方法,当在最后一页执行删除操作时,基于全选按钮状态及当前页码值,刷新页面
function doRefreshAfterDeleteOK() {
var pageCount = $("#pageId").data("pageCount");
var pageCurrent = $("#pageId").data("pageCurrent");
var checked = $("#checkAll").prop("checked");
if (pageCurrent == pageCount && checked && pageCurrent > 1) {
pageCurrent--;
$("#pageId").data("pageCurrent", pageCurrent);
}
doGetObjects();
}
五. 数据添加实现
5.1 业务时序分析
5.2 服务端实现
5.2.1 Dao接口实现
1)在SysLogDao接口中添加用于实现日志信息持久化的方法
int insertObject(SysLog entity);
2)在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>
5.2.2 Service接口实现
1)在SysLogService接口中,添加保存日志信息的方法
void saveObject(SysLog entity);
2)在SysLogServiceImpl类中添加,保存日志的方法实现
@Override
public void saveObject(SysLog entity) {
sysLogDao.insertObject(entity);
}
5.2.3 日志切面Aspect实现
在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业务层对象对日志日志信息做进一步处理
1)springboot工程中应用AOP时,首先要添加如下依赖(假如有则无需添加)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2)定义日志切面类对象,通过环绕通知处理日志记录操作
@Aspect
@Component
public class SysLogAspect {
private Logger log=LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private SysLogService sysLogService;
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
public void logPointCut(){}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint //连接点
jointPoint) throws Throwable{
long startTime=System.currentTimeMillis();
//执行目标方法(result为目标方法的执行结果)
Object result=jointPoint.proceed();
long endTime=System.currentTimeMillis();
long totalTime=endTime-startTime;
log.info("方法执行的总时长为:"+totalTime);
saveSysLog(jointPoint,totalTime);
return result;
}
private void saveSysLog(ProceedingJoinPoint point,
long totleTime) throws NoSuchMethodException, SecurityException, JsonProcessingException{
//1.获取日志信息
MethodSignature ms= (MethodSignature)point.getSignature();
Class<?> targetClass=point.getTarget().getClass();
String className=targetClass.getName();
//获取接口声明的方法
String methodName=ms.getMethod().getName();
Class<?>[] parameterTypes=ms.getMethod().getParameterTypes();
//获取目标对象方法(AOP版本不同,可能获取方法对象方式也不同)
Method targetMethod=targetClass.getDeclaredMethod(
methodName,parameterTypes);
//获取用户名,学完shiro再进行自定义实现,没有就先给固定值
String username=ShiroUtils.getPrincipal().getUsername();
//获取方法参数
Object[] paramsObj=point.getArgs();
System.out.println("paramsObj="+paramsObj);
//将参数转换为字符串
String params=new ObjectMapper()
.writeValueAsString(paramsObj);
//2.封装日志信息
SysLog log=new SysLog();
log.setUsername(username);//登陆的用户
//假如目标方法对象上有注解,我们获取注解定义的操作值
RequiredLog requestLog=
targetMethod.getDeclaredAnnotation(RequiredLog.class);
if(requestLog!=null){
log.setOperation(requestLog.value());
}
log.setMethod(className+"."+methodName);//className.methodName()
log.setParams(params);//method params
log.setIp(IPUtils.getIpAddr());//ip 地址
log.setTime(totleTime);//
log.setCreateDate(new Date());
//3.保存日志信息
sysLogService.saveObject(log);
}
}
方法中用到的ip地址获取需要提供一个如下的工具类
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
public static String getIpAddr() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
return ip;
}
}
原理分析,如图所示