《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/yjt520557/article/details/85099115/ 「qq_1959227206」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

1.简介

我在使用spring完成项目的时候需要完成记录日志,我开始以为Spring 的AOP功能,就可以轻松解决,半个小时都不用,可是经过一番了解过后,发现一般的日志记录,只能记录一些简单的操作,例如表名、表名称等记录不到。

这个时侯就用到了自定义注解,把想要记录的内容放在注解中,通过切入点来获取到注解参数,然后将参数插入数据库记录

2.Spring AOP

对于Spring Aop的基本介绍大家可以看看:

https://blog.csdn.net/yjt520557/article/details/84833508

这里是为了方便大家理解如何实现给大家解释一下

2.1.关于Spring AOP的一些术语

  • 切面(Aspect):在Spring AOP中,切面可以使用通用类或者在普通类中以@Aspect 注解(@AspectJ风格)来实现
  • 连接点(Joinpoint):在Spring AOP中一个连接点代表一个方法的执行
  • 通知(Advice):在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括”around”、”before”和”after”等通知。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链
  • 切入点(Pointcut):定义出一个或一组方法,当执行这些方法时可产生通知,Spring缺省使用AspectJ切入点语法。

通知类型

  • 前置通知(@Before):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)
  • 返回后通知(@AfterReturning):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
  • 抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
  • 后通知(@After):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
  • 环绕通知(@Around):包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型,环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行

2.2.Spring AOP配置有两种风格:

  • XML风格 = 采用声明形式实现Spring AOP
  • AspectJ风格 = 采用注解形式实现Spring AOP

3.首先自定义注解

定义一个日志描述和一个表名这里根据需要自定义注解

package com.ywj.log;

import java.lang.annotation.*;

/**
* ClassName Crmlog
* AOP日志记录 自定义注解类
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemCrmlog {
/**
* 日志描述
* 对于什么表格进行了什么操作
*/
String description() default "";

/**
* 操作了的表名
* @return
*/
String tableName() default "";
}

3.1.定义切面类,从切入点获取注解信息保存到数据库

对于一些可能碰到的问题我在方法的注释里都有解决办法,大家注意一下,这里我对于方法报错也有处理方法

这里是对于切面类里使用到的两个类解释:

AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环绕增强时,使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint访问到连接点上下文的信息。我们先来了解一下这两个接口的主要方法:

1)JoinPoint

  • java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
  • Signature getSignature() :获取连接点的方法签名对象;
  • java.lang.Object getTarget() :获取连接点所在的目标对象;
  • java.lang.Object getThis() :获取代理对象本身;

2)ProceedingJoinPoint

ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法:

  • java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法;
  • java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。
package com.ywj.log;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.ywj.log.biz.Sys_logBiz;
import com.ywj.log.dao.Sys_logDao;
import com.ywj.login.biz.Sys_UserBiz;
import com.ywj.login.dao.Sys_UserDao;
import com.ywj.login.dao.Sys_righDao;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;


/**
* @ClassName SystemLogAspect
* @Author Administrator
* @Describe 定义切入面类
*/
@Aspect
@Component
public class SystemLogAspect {


/**
* 注解Pointcut切入点
* 定义出一个或一组方法,当执行这些方法时可产生通知
* 指向你的切面类方法
* 由于这里使用了自定义注解所以指向你的自定义注解
*/
@Pointcut("@annotation(com.ywj.log.SystemCrmlog)")
public void crmAspect() {
}


/**
*抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
* 注意在这里不能使用ProceedingJoinPoint
* 不然会报错ProceedingJoinPoint is only supported for around advice
* throwing注解为错误信息
* @param joinPoint
* @param ex
*/
@AfterThrowing(value="crmAspect()", throwing="ex")
public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) throws Exception {
HttpServletRequest httpServletRequest = getHttpServletRequest();
//获取管理员用户信息\
WebUtil webUtil = new WebUtil();
Map<String, Object> user = webUtil.getUser(httpServletRequest);
CrmLogMessage log=new CrmLogMessage();
//获取需要的信息
String context=getServiceMthodDescription(joinPoint);
String usr_name="";
String rolename="";
if(user!=null){
usr_name = user.get("usr_name").toString();
rolename=user.get("rolename").toString();
}
//管理员姓名
log.setUserName(usr_name);
//角色名
log.setUserRole(rolename);
//日志信息
log.setContent(usr_name+context);
//设置参数集合
log.setRemarks(getServiceMthodParams(joinPoint));
//设置表名
log.setTableName(getServiceMthodTableName(joinPoint));
//操作时间
SimpleDateFormat sif=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.setDateTime(sif.format(new Date()));
//设置ip地址
log.setIp(httpServletRequest.getRemoteAddr());
//设置请求地址
log.setRequestUrl(httpServletRequest.getRequestURI());
//执行结果
log.setResult("执行失败");
//错误信息
log.setExString(ex.getMessage());
//将数据保存到数据库
Sys_logDao sysLogDao=new Sys_logDao();
sysLogDao.addSys_log(log);
}


/**
* 返回后通知(@AfterReturning):在某连接点(joinpoint)
* 正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
* 方法执行完毕之后
* 注意在这里不能使用ProceedingJoinPoint
* 不然会报错ProceedingJoinPoint is only supported for around advice
* crmAspect()指向需要控制的方法
* returning 注解返回值
* @param joinPoint
* @param returnValue 返回值
* @throws Exception
*/
@AfterReturning(value = "crmAspect()",returning = "returnValue")
public void doCrmLog(JoinPoint joinPoint,Object returnValue) throws Exception {
HttpServletRequest httpServletRequest = getHttpServletRequest();
//获取管理员用户信息
WebUtil webUtil = new WebUtil();
Map<String, Object> user = webUtil.getUser(httpServletRequest);
CrmLogMessage log=new CrmLogMessage();
String context=getServiceMthodDescription(joinPoint);

String usr_name="";
String rolename="";
if(user!=null){
usr_name = user.get("usr_name").toString();
rolename=user.get("rolename").toString();
}
//管理员姓名
log.setUserName(usr_name);
//角色名
log.setUserRole(rolename);
//日志信息
log.setContent(usr_name+context);
//设置参数集合
log.setRemarks(getServiceMthodParams(joinPoint));
//设置表名
log.setTableName(getServiceMthodTableName(joinPoint));
//操作时间
SimpleDateFormat sif=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.setDateTime(sif.format(new Date()));
//设置ip地址
log.setIp(httpServletRequest.getRemoteAddr());
//设置请求地址
log.setRequestUrl(httpServletRequest.getRequestURI());
if(returnValue!=null){
if(returnValue instanceof List){
List ls= (List) returnValue;
if(ls.size()>0){
log.setResult("执行成功");
}else{
log.setResult("执行成功");
}
}else if(returnValue instanceof Boolean){
Boolean falg= (Boolean) returnValue;
if(falg){
log.setResult("执行成功");
}else{
log.setResult("执行失败");
}
}else if(returnValue instanceof Integer){
Integer i= (Integer) returnValue;
if(i>0){
log.setResult("执行成功");
}else{
log.setResult("执行失败");
}
}else{
log.setResult("执行成功");
}

}
//将数据保存到数据库
Sys_logDao sysLogDao=new Sys_logDao();
sysLogDao.addSys_log(log);
}


/**
*获取自定义注解里的日志描述
* @param joinPoint
* @return 返回注解里面的日志描述
* @throws Exception
*/
private String getServiceMthodDescription(JoinPoint joinPoint)
throws Exception {
//类名
String targetName = joinPoint.getTarget().getClass().getName();
//方法名
String methodName = joinPoint.getSignature().getName();
//参数
Object[] arguments = joinPoint.getArgs();
//通过反射获取示例对象
Class targetClass = Class.forName(targetName);
//通过实例对象方法数组
Method[] methods = targetClass.getMethods();
String description = "";
for(Method method : methods) {
//判断方法名是不是一样
if(method.getName().equals(methodName)) {
//对比参数数组的长度
Class[] clazzs = method.getParameterTypes();
if(clazzs.length == arguments.length) {
//获取注解里的日志信息
description = method.getAnnotation(SystemCrmlog.class).description();
break;
}
}
}
return description;
}

/**
*获取自定义注解里的表名
* @param joinPoint
* @return 返回注解里的表名字
* @throws Exception
*/
private String getServiceMthodTableName(JoinPoint joinPoint)
throws Exception {
//类名
String targetName = joinPoint.getTarget().getClass().getName();
//方法名
String methodName = joinPoint.getSignature().getName();
//参数
Object[] arguments = joinPoint.getArgs();
//通过反射获取示例对象
Class targetClass = Class.forName(targetName);
//通过实例对象方法数组
Method[] methods = targetClass.getMethods();
//表名
String tableName = "";
for (Method method : methods) {
//判断方法名是不是一样
if (method.getName().equals(methodName)) {
//对比参数数组的长度
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
//获取注解里的表名
tableName = method.getAnnotation(SystemCrmlog.class).tableName();
break;
}
}
}
return tableName;
}

/**
* 获取json格式的参数用于存储到数据库中
* @param joinPoint
* @return
* @throws Exception
*/
private String getServiceMthodParams(JoinPoint joinPoint)
throws Exception {
Object[] arguments = joinPoint.getArgs();
ObjectMapper om=new ObjectMapper();
return om.writeValueAsString(arguments);
}

/**
* 获取当前的request
* 这里如果报空指针异常是因为单独使用spring获取request
* 需要在配置文件里添加监听
* <listener>
* <listener-class>
* org.springframework.web.context.request.RequestContextListener
* </listener-class>
* </listener>
* @return
*/
public HttpServletRequest getHttpServletRequest(){
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes)ra;
HttpServletRequest request = sra.getRequest();
return request;
}

}

每个切面传递的数据的都不一样,最终决定,获取切面的所有参数,转成json字符串,保存到数据库中。

相关类:

日志信息类

package com.ywj.log;

/**
* @ClassName CrmLogMessage
* @Author Administrator
* @Describe 数据库日志类
*/
public class CrmLogMessage {
private Integer logid;//日志id
private String UserName;//管理员姓名
private String UserRole;//管理员角色
private String Content;//日志描述
private String Remarks;//参数集合
private String TableName;//表格名称
private String DateTime;//操作时间
private String resultValue;//返回值
private String ip;//ip地址
private String requestUrl;//请求地址
private String result;//操作结果
private String ExString;//错误信息

public CrmLogMessage() {
}

@Override
public String toString() {
return "CrmLogMessage{" +
"logid=" + logid +
", UserName='" + UserName + '\'' +
", UserRole='" + UserRole + '\'' +
", Content='" + Content + '\'' +
", Remarks='" + Remarks + '\'' +
", TableName='" + TableName + '\'' +
", DateTime='" + DateTime + '\'' +
", resultValue='" + resultValue + '\'' +
", ip='" + ip + '\'' +
", requestUrl='" + requestUrl + '\'' +
", result='" + result + '\'' +
", ExString='" + ExString + '\'' +
'}';
}

public CrmLogMessage(Integer logid, String userName, String userRole, String content, String remarks, String tableName, String dateTime, String resultValue, String ip, String requestUrl, String result, String exString) {
this.logid = logid;
UserName = userName;
UserRole = userRole;
Content = content;
Remarks = remarks;
TableName = tableName;
DateTime = dateTime;
this.resultValue = resultValue;
this.ip = ip;
this.requestUrl = requestUrl;
this.result = result;
ExString = exString;
}

}

用来获取登录用户信息的帮助类:

package com.ywj.log;

import com.base.web.BaseAction;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
* @ClassName WebUtil
* @Author Administrator
* @Describe 日志帮助类 用来获取session中的用户信息来存入数据库
*/
public class WebUtil {


/**
* 从session中获取到用户对象
* @return
*/
public Map<String, Object> getUser(HttpServletRequest request){
Map<String, Object> attribute=null;
if(request!=null){
Object user = request.getSession().getAttribute(Constans.USER_KEY);
attribute = (Map<String, Object>) user;}
return attribute;
}

}

在你的spring-context.xml中配置

 <!-- 启动对@AspectJ注解的支持  -->
<aop:aspectj-autoproxy proxy-target-class="true" />
<!-- 自动扫描包路径 -->
<!--你需要刚才的切面类的包路径-->
<context:component-scan base-package="com.ywj.log" />
<!--你需要注解方法的包路径-->
<context:component-scan base-package="com.*.*.biz.impl" />

然后在你需要记录的方法上加上注解

@SystemCrmlog(description = "进行了登录操作",tableName =Constans.USER_TABLENAME)

效果这里表名使用了常量类

对于一些表的信息可以写一个常量类

然后执行登录操作数据库记录为:

文章目录
  1. 1. 1.简介
  2. 2. 2.Spring AOP
    1. 2.1. 2.1.关于Spring AOP的一些术语
      1. 2.1.1. 通知类型
    2. 2.2. 2.2.Spring AOP配置有两种风格:
  3. 3. 3.首先自定义注解
    1. 3.1. 3.1.定义切面类,从切入点获取注解信息保存到数据库
      1. 3.1.1. 1)JoinPoint
      2. 3.1.2. 2)ProceedingJoinPoint
    2. 3.2. 相关类: