1

1 日志管理设计

日志页面查询、日志删除、日志添加的实现。

1.1 数据库导入

用户行为日志表设计,针对增删改查数据核对。

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='系统日志';

1.2 页面设计

image.png

1.3 分页API设计

image.png

1.4 分页业务时序分析

image.png

2 日志管理列表页面呈现

2.1 PageController的实现

基于日志管理的请求业务,在PageController中添加doLogUI方法,doPageUI方法分别用于返回日志列表页面,日志分页页面。
第一步:在PageController中定义返回日志列表的方法。代码如下:

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

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

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

2.2 客户端实现

2.2.1 日志页面跳转

首先准备日志列表页面(/templates/pages/sys/log_list.html),然后在starter.html页面中点击日志管理菜单时异步加载日志列表页面。

找到项目中的starter.html 页面,页面加载完成以后,注册日志管理菜单项的点击事件,当点击日志管理时,执行事件处理函数。关键代码如下:

$(function(){
     doLoadUI("load-log-id","log/log_list")
})
function doLoadUI(id,url){
      $("#"+id).click(function(){
        $("#mainContentId").load(url);
    });
}

其中,load函数为jquery中的ajax异步请求函数。

2.2.2 日志页面分页异步加载

$(function(){
    $("#pageId").load("doPageUI");
});

3 业务实现

3.1 日志管理实现

查询:

数据架构分析
image.png
日志分页架构分析
image.png
时序图分析:
image.png

删除:

日志删除架构分析:
image.png
日志删除时序图分析:
image.png

第一步:创建SysLog实体类

package com.cy.pj.sys.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
//实现此接口的对象可以进行序列化和反序列化
//1)序列化:将对象转化为字节的过程,转换以后便于通过网络进行传输或存储到相关介质中
//2)反序列化:将字节转换为对象的过程
//建议:在java中所有用于存储数据的对象都实现Serializable接口
@Data
public class SysLog implements Serializable {
    private static final long serialVersionUID = 1L;
 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;
}

第二步:创建SysLogDao层

package com.cy.pj.sys.dao;
import com.cy.pj.sys.pojo.SysLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface SysLogDao {
    int insertObject(SysLog entity);
     int deleteObjects(@Param("ids") Integer... ids);
     /**
     * @param username 查询条件(例如查询哪个用户的日志信息)
     * @return 总记录数(基于这个结果可以计算总页数)
     */ //int getRowCount(@Param("username") String username);
     /**
     * @param username 查询条件(例如查询哪个用户的日志信息)
     * @param startIndex 当前页的起始位置
     * @param pageSize 当前页的页面大小
     * @return 当前页的日志记录信息
     * 数据库中每条日志信息封装到一个SysLog对象中
     */
     List<SysLog> findPageObjects(String username);
}
////            @Param("startIndex")Integer startIndex,
////            @Param("pageSize")Integer pageSize);

第三步:创建mapper文件映射

<?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">
     <insert id="insertObject">
         insert into sys_logs
         (username,operation,method,params,time,ip,createdTime)
         values
         (#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
     </insert>
     <delete id="deleteObjects">
     delete from sys_Logs
            <where>
                 <if test="ids!=null and ids.length>0">
                 id in
                 <foreach collection="ids" open="(" close=")" separator="," item="id">
                 #{id}
                 </foreach>
                 </if> 
                 or 1=2
            </where>
     </delete>
     <sql id="queryWhereId">
         from sys_Logs
         <where>
             <if test="username!=null and username!=''">
                username like concat("%",#{username},"%")
             </if>
         </where> 
     </sql>
     <select id="getRowCount" resultType="int">
         select count(*)
         <include refid="queryWhereId"/>
     </select>
    <!--    <select id="findPageObjects"-->
    <!--            resultType="com.cy.pj.sys.pojo.SysLog">-->
    <!--        select *-->
    <!--        <include refid="queryWhereId"/>-->
    <!--        order by createdTime desc-->
    <!--        limit #{startIndex},#{pageSize}-->
    <!--    </select>-->
     <select id="findPageObjects"
        resultType="com.cy.pj.sys.pojo.SysLog">
         select *
         <include refid="queryWhereId"/>
         order by createdTime desc
     </select>
</mapper>

第四步:创建service接口及实现类
创建pojo类PageOject类:

package com.cy.pj.common.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
//借助此对象封装业务逻辑结果
@Data
@NoArgsConstructor
public class PageObject<T> implements Serializable {//类泛型:类名<泛型> 约束类中属性,方法参数以及返回值类型
     private static final long serialVersionUID = -3130527491950235344L;
     /**当前页的页码值*/
     private Integer pageCurrent=1;
     /**页面大小*/
     private Integer pageSize=3;
     /**总行数(通过查询获得)*/
     private Integer rowCount=0;
     /**总页数(通过计算获得)*/
     private Integer pageCount=0;
     /**当前页记录*/
     private List<T> records;
     public PageObject(Integer pageCurrent, Integer pageSize, Integer rowCount, List<T> records) {
            this.pageCurrent = pageCurrent;
     this.pageSize = pageSize;
     this.rowCount = rowCount;
     this.pageCount=rowCount/pageSize;
     this.records = records;
     if(this.rowCount%this.pageSize!=0)this.pageCount++;
     }
}

创建SysLogService接口:

package com.cy.pj.sys.servive;
import com.cy.pj.common.pojo.PageObject;
import com.cy.pj.sys.pojo.SysLog;
public interface SysLogService {
    void saveObject(SysLog entity);
     int deleteObjects(Integer... Ids);
     /**
     * @param username 基于条件查询时的参数名
     * @param pageCurrent 当前的页码值
     * @return 当前页记录+分页信息
     */
     PageObject<SysLog> findPageObjects(
                String username,
     Integer pageCurrent);
}

创建SysLogServiceImpl实现类:

package com.cy.pj.sys.servive.impl;
import com.cy.pj.common.annotation.RequiredLog;
import com.cy.pj.common.exception.ServiceException;
import com.cy.pj.sys.dao.SysLogDao;
import com.cy.pj.common.pojo.PageObject;
import com.cy.pj.sys.pojo.SysLog;
import com.cy.pj.sys.servive.SysLogService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import org.apache.shiro.authz.annotation.RequiresPermissions;
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;
     @Override
     public void saveObject(SysLog entity) {
            sysLogDao.insertObject(entity);
     }
        //@requirespermission注解描述的方法为一个权限切入点方法,当登录用户访问此方法时需要授权。
         //那如何检测用户是否有访问此方法的权限?
         //第一:获取访问此方法时需要的权限标识:"sys:log:delete"
         //第二:获取登陆用户拥有的菜单访问的权限标识
         //第三:判定用户拥有的权限标识中是否包含访问时需要的授权标识,假如包含则授权访问。
         @RequiresPermissions("sys:log:delete")
        @RequiredLog("日志删除")
        @Override
         public int deleteObjects(Integer... ids) {
                //1,参数校验
         if(ids==null||ids.length==0)
                    throw new IllegalArgumentException("必须提供正确的id值");
         //2,基于id删除日志
         int rows=sysLogDao.deleteObjects(ids);
         //3,检验结果并返回
         if(rows==0)
                    throw new ServiceException("记录可能已经不存在");
         return rows;
         }
        @RequiredLog("日志查询")
        @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=5;
         Page<SysLog> page=PageHelper.startPage(pageCurrent, pageSize);
         //3.2)计算startIndex
        //            int startIndex=(pageCurrent-1)*pageSize;
         //3.3)执行当前数据的查询操作
        //            List<SysLog> records=
        //                    sysLogDao.findPageObjects(name, startIndex, pageSize);
         List<SysLog> records=
                        sysLogDao.findPageObjects(name);
         //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 new PageObject<>(pageCurrent,pageSize,(int)page.getTotal(),records);
         }
    }

设置自定义异常处理ServiceException:

package com.cy.pj.common.exception;
public class ServiceException extends RuntimeException {
    private static final long serialVersionUID = 7793296502722655579L;
     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
     }
}

第五步:创建Controller类
创建pojo返回值对象JsonResult:

package com.cy.pj.common.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
//封装服务端响应到客户端的数据
@Data
@NoArgsConstructor
public class JsonResult implements Serializable {
    private static final long serialVersionUID = 8518978402852081033L;
 //SysResult/Result/R
 /**状态码*/
 private Integer state=1;//1表示SUCCESS,0表示ERROR
 /**状态信息*/
 private String message="ok";
 /**正确数据*/
 private Object data;
 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();
 }
}

创建SysLogController:

package com.cy.pj.sys.controller;
import com.cy.pj.common.annotation.RequiredLog;
import com.cy.pj.common.pojo.JsonResult;
import com.cy.pj.common.pojo.PageObject;
import com.cy.pj.sys.pojo.SysLog;
import com.cy.pj.sys.servive.SysLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/log/")
public class SysLogController {
    @Autowired
    private SysLogService sysLogService;
    @RequestMapping("doDeleteObjects")
    public JsonResult doDeleteObjects(Integer... ids){
        sysLogService.deleteObjects(ids);
        return new JsonResult("Delete ok");
    }
    //在Controller类中添加分页请求处理方法,代码参考如下:
    @RequestMapping("doFindPageObjects")
    public JsonResult doFindPageObjects(String username, Integer pageCurrent) {
        PageObject<SysLog> pageObject =
                sysLogService.findPageObjects(username, pageCurrent);
        return new JsonResult(pageObject);
    }
}

设置全局异常控制:

package com.cy.pj.common.web;
import com.cy.pj.common.pojo.JsonResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    //JDK中的自带的日志API
     @ExceptionHandler(RuntimeException.class)
        @ResponseBody
     public JsonResult doHandleRuntimeException(
                RuntimeException e){
            e.printStackTrace();//也可以写日志
         log.error("exception msg {}"+e.getMessage());
         //异常信息
         return new JsonResult(e);//封装
     }
        @ExceptionHandler(ShiroException.class)
        @ResponseBody
     public JsonResult doHandleShiroException(
                    ShiroException e) {
         JsonResult r=new JsonResult();
         r.setState(0);
         if(e instanceof UnknownAccountException) {
                    r.setMessage("账户不存在");
         }else if(e instanceof LockedAccountException) {
                    r.setMessage("账户已被禁用");
         }else if(e instanceof IncorrectCredentialsException) {
                    r.setMessage("密码不正确");
         }else if(e instanceof AuthorizationException) {
                    r.setMessage("没有此操作权限");
         }else {
                    r.setMessage("系统维护中");
         }
                e.printStackTrace();
         return r;
     }
}

image.png

日志管理AOP实现

第一步:自定义注解RequiredLog

package com.cy.pj.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)//定义我们的注解可以修饰谁
@Retention(RetentionPolicy.RUNTIME)//定义我们的注解何时有效
public @interface RequiredLog {
    String value() default "";
}

第二步:切面类SysLogAspect实现

package com.cy.pj.common.aspect;
import com.cy.pj.common.annotation.RequiredLog;
import com.cy.pj.common.util.IPUtils;
import com.cy.pj.sys.pojo.SysLog;
import com.cy.pj.sys.servive.SysLogService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Date;
/**
 * @Aspect 注解描述的对象为一个切面对象,在切面对象中定义
 * 1)切入点(Pointcut):织入扩展功能的一些连接点的集合
 * 2)通知方法(Advice):封装了扩展逻辑的方法
 */
@Slf4j
@Aspect
@Component
public class SysLogAspect {
    //通过Pointcut定义一个切入点,@annotation方式为定义切入点的一种方式,
     //在这里表示业务对象中由com.cy.pj.common.annotation.RequiredLog注解描述的方法为一些切入点方法
     @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
        public void doLog(){}//doLog方法仅仅是@Pointcut注解的一个载体,方法体内不需要写任何内容
         /**
         * @Around 注解描述的方法可以在目标方法执行之前和之后做功能扩展
         * @param joinPoint 封装了目标方法信息的一个对象(连接点对象)
         * @return 目标方法的执行结果
         * @throws Throwable
         */ @Around("doLog()")
        public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
            try {
                long t1 = System.currentTimeMillis();
             Object result = joinPoint.proceed();//去调用目标方法,其返回值为目标方法返回值
             long t2 = System.currentTimeMillis();
             System.out.println("time:" + (t2 - t1));
             //将正常的用户行为日志写入到数据库
             saveSysLog(joinPoint, (t2 - t1));
             return result;
             }catch(Throwable e){
                        //saveErrorLog(...);也可以将错误日志写入到数据库
             logError(joinPoint,e.getMessage());
             throw e;
             }
        }
        //将错误日志进行输出并记录
     private void logError(ProceedingJoinPoint joinPoint,String exceptionMsg) throws JsonProcessingException {
            String targetClassName=joinPoint.getTarget().getClass().getName();
             String methodName=joinPoint.getSignature().getName();
             String params=new ObjectMapper().writeValueAsString(joinPoint.getArgs());
             log.error("error.msg->{}->{}->{}",targetClassName+"."+methodName,params,exceptionMsg);
             }
        @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);
             //获取操作名(RequiredLog中operation的值)
             String operation= requiredLog.value();
             //2.封装日志信息
             SysLog entity=new SysLog();
             entity.setUsername("cgb");//将来这个位置为登录用户名
             entity.setIp(IPUtils.getIpAddr());
             entity.setOperation(operation);//为目标方法指定的一个名字
             entity.setMethod(targetClass.getName()+"."+targetMethod.getName());//类全名+方法名
             //entity.setParams(Arrays.toString(joinPoint.getArgs()));//调用方法时传递实际参数
             entity.setParams(new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
             entity.setTime(time);
             entity.setCreatedTime(new Date());
             //3.保存用户行为值
             //sysLogService.saveObject(entity);
             //异步写日志(自己new thread,借助池中线程,但非tomcat线程池中线程)
             new Thread(){//1M
             @Override
             public void run() {
                            sysLogService.saveObject(entity);
             }
            }.start();
     }
}

原理分析:
image.png


木安
13 声望6 粉丝