版本信息
- JDK21
- SpringBoot3.3.0
- Knife4.5.0
- Shiro2.0.1
- Jackson2.17.1
- Maven3.9.6
- IDEA2024.1
一、需求
通过自定义日志注解,实现对请求信息的记录。按照请求方法(GET/POST等),请求地址、请求人员、请求来源等不同来进行记录,方便日后进行日志审计。
二、依赖
在SpringBoot2.x与SpringBoot3.x中只针对于javax...
和jakarta...
的引用的不同,其余均可复用,这里以最新当前版本信息为准。
以下仅列出该项目涉及到的部分核心依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<!-- 更新log4j2版本包:https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<log4j2.version>2.23.1</log4j2.version>
<!-- 打包时跳过测试环境 -->
<skipTests>true</skipTests>
...
<!--
SpringBoot3.x集成knife4j
knife4j-openapi3-jakarta-spring-boot-starter:https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter
-->
<knife4j.version>4.5.0</knife4j.version>
<!--
SpringBoot集成shiro:https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring
shiro-ehcache:https://mvnrepository.com/artifact/org.apache.shiro/shiro-ehcache
-->
<shiro-spring.version>2.0.1</shiro-spring.version>
<!-- jackson:https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<jackson.version>2.17.1</jackson.version>
...
</properties>
<dependencies>
...
<!-- Knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- shiro 依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<classifier>jakarta</classifier>
<version>${shiro-spring.version}</version>
<!-- 排除仍使用了javax.servlet的依赖 -->
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro-spring.version}</version>
</dependency>
<!-- 引入适配jakarta的依赖包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>${shiro-spring.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
<version>${shiro-spring.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>
...
</dependencies>
三、步骤
1.自定义常量扩展类<ConstantsExpand.java
>
package cn.keyidea.common.constant;
/**
* 服务端自定义变量(扩展)
*
* @author qyd
* @date 2022-10-17
*/
public final class ConstantsExpand
{
/**
* 标记类是否进行日志记录
* 说明:不记录的情况主要是针对在操作日志类本身的查询
*/
public interface SysLogFlag
{
int FLASE = 0; // 不记录
int TRUE = 1; // 记录
}
/**
* 业务来源
*/
public interface SourceType
{
int INSIDE_SYS = 0; // 系统日志(系统内日志,系统产生,如定时任务、告警等)
int INSIDE_USER = 1; // 用户日志(系统内日志,用户行为日志,用户操作产生的日志)
int OUTSIDE = 2; // 系统外日志(第三方日志,可根据不同第三方继续细化日志来源)
}
/**
* 业务类型
*/
public interface ServiceType
{
int ADD = 1; // 新增
int DELETE = 2; // 删除
int UPDATE = 3; // 更新
int QUERY = 4; // 查询
int LOGIN = 5; // 登陆
int LOGOUT = 6; // 登出
int EXPORT = 7; // 导出
int IMPORT = 8; // 导入
int SYNC = 9; // 同步
int UPLOAD = 10; // 上传
int DOWNLOAD = 11; // 下载
int OTHER = 99; // 其他
}
}
2.日志表设计
日志类实体<SysLog.java
>
package cn.keyidea.sys.entity;
import cn.keyidea.common.constant.ConstantsExpand;
import cn.keyidea.common.constant.StatusCode;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 日志记录
*
* @author qyd
* @date 2022-10-17
*/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("sys_log")
@Schema(name = "SysLog", description = "系统日志")
@Entity
@Table(name = "sys_log", indexes = {
@Index(name = "index_user_name", columnList = "user_name"),
@Index(name = "index_module", columnList = "module"),
@Index(name = "index_service_type", columnList = "service_type"),
@Index(name = "index_source_type", columnList = "source_type"),
@Index(name = "index_request_method", columnList = "request_method"),
@Index(name = "index_ip", columnList = "ip"),
@Index(name = "index_create_date", columnList = "create_date"),
@Index(name = "index_response_code", columnList = "response_code"),
})
@Comment("系统日志表")
public class SysLog implements Serializable
{
@Serial
private static final long serialVersionUID = 1L;
/**
* GeneratedValuez注解中strategy属性:提供四种值
* -AUTO主键由程序控制, 是默认选项 ,不设置就是这个
* -IDENTITY 主键由数据库生成, 采用数据库自增长, Oracle不支持这种方式
* -SEQUENCE 通过数据库的序列产生主键, MYSQL 不支持
* -Table 提供特定的数据库产生主键, 该方式更有利于数据库的移植
*/
@Schema(name = "id", description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@Id
@TableId(value = "id", type = IdType.AUTO)
@Column(name = "id", columnDefinition = "bigint(20) unsigned not null COMMENT '主键ID'")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 用户名称,如果是第三方调用,不走验证,可以缺省
*/
@Schema(name = "userName", description = "用户名称", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "user_name", columnDefinition = "varchar(20) COMMENT '用户名称'")
private String userName;
@Schema(name = "module", description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "module", columnDefinition = "varchar(50) not null COMMENT '模块名称'")
private String module;
@Schema(name = "serviceDesc", description = "业务描述", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "service_desc", columnDefinition = "varchar(100) not null COMMENT '业务描述'")
private String serviceDesc;
/**
* 业务类型,参见{@link ConstantsExpand.ServiceType}
*/
@Schema(name = "serviceType", description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "service_type", columnDefinition = "int(10) not null COMMENT '业务类型'")
private Integer serviceType;
/**
* 业务来源,参见{@link ConstantsExpand.SourceType}
* 0-系统日志(系统内日志,系统产生,如定时任务、告警等)
* 1-用户日志(系统内日志,用户行为日志,用户操作产生的日志)
* 2-系统外日志(第三方日志,可根据不同第三方继续细化日志来源)
*/
@Schema(name = "sourceType", description = "业务来源:0-系统日志(系统内日志,系统产生,如定时任务、告警等)、1-用户日志(系统内日志,用户行为日志,用户操作产生的日志)、2-系统外日志(第三方日志,可根据不同第三方继续细化日志来源)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@Column(name = "source_type", columnDefinition = "int(2) not null COMMENT '业务来源:0-系统日志(系统内日志,系统产生,如定时任务、告警等)、1-用户日志(系统内日志,用户行为日志,用户操作产生的日志)、2-系统外日志(第三方日志,可根据不同第三方继续细化日志来源)'")
private Integer sourceType;
@Schema(name = "requestMethod", description = "请求方式", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "request_method", columnDefinition = "varchar(10) not null COMMENT '请求方式'")
private String requestMethod;
@Schema(name = "className", description = "类名", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "class_name", columnDefinition = "varchar(200) not null COMMENT '类名'")
private String className;
@Schema(name = "methodName", description = "方法名称", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "method_name", columnDefinition = "varchar(100) not null COMMENT '方法名称'")
private String methodName;
@Schema(name = "payload", description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "payload", columnDefinition = "varchar(1000) COMMENT '请求参数'")
private String payload;
@Schema(name = "ip", description = "IP地址", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "ip", columnDefinition = "varchar(20) not null COMMENT 'IP地址'")
private String ip;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(name = "createDate", description = "执行时间", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "create_date", columnDefinition = "datetime not null COMMENT '执行时间'")
private Date createDate;
@Schema(name = "responseTime", description = "响应时间,单位:毫秒", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "response_time", columnDefinition = "int(10) not null COMMENT '响应时间,单位:毫秒'")
private Integer responseTime;
/**
* 执行状态,参见:{@link StatusCode}
*/
@Schema(name = "responseCode", description = "执行状态:1000-成功、1001-非法字段、1002-系统忙、1003-无接口访问权限、1004-请求失败、1005-未授权、2001-TOKEN失效", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "response_code", columnDefinition = "int(2) not null COMMENT '执行状态:1000-成功、1001-非法字段、1002-系统忙、1003-无接口访问权限、1004-请求失败、1005-未授权、2001-TOKEN失效'")
private Integer responseCode;
/**
* 说明,desc与describe是MySQL关键字
*/
@Schema(name = "descInfo", description = "执行描述", requiredMode = Schema.RequiredMode.REQUIRED)
@Column(name = "desc_info", columnDefinition = "varchar(2000) COMMENT '执行描述'")
private String descInfo;
}
日志表
CREATE TABLE `sys_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_name` varchar(20) DEFAULT NULL COMMENT '用户名称',
`module` varchar(50) NOT NULL COMMENT '模块名称',
`service_desc` varchar(100) NOT NULL COMMENT '业务描述',
`service_type` int(10) NOT NULL COMMENT '业务类型',
`source_type` int(2) NOT NULL COMMENT '业务来源:0-系统日志(系统内日志,系统产生,如定时任务、告警等)、1-用户日志(系统内日志,用户行为日志,用户操作产生的日志)、2-系统外日志(第三方日志,可根据不同第三方继续细化日志来源)',
`request_method` varchar(10) NOT NULL COMMENT '请求方式',
`class_name` varchar(200) NOT NULL COMMENT '类名',
`method_name` varchar(100) NOT NULL COMMENT '方法名称',
`payload` varchar(1000) DEFAULT NULL COMMENT '请求参数',
`ip` varchar(20) NOT NULL COMMENT 'IP地址',
`create_date` datetime NOT NULL COMMENT '执行时间',
`response_time` int(10) NOT NULL COMMENT '响应时间,单位:毫秒',
`response_code` int(2) NOT NULL COMMENT '执行状态:1000-成功、1001-非法字段、1002-系统忙、1003-无接口访问权限、1004-请求失败、1005-未授权、2001-TOKEN失效',
`desc_info` varchar(2000) DEFAULT NULL COMMENT '执行描述',
PRIMARY KEY (`id`),
KEY `index_user_name` (`user_name`),
KEY `index_module` (`module`),
KEY `index_service_type` (`service_type`),
KEY `index_source_type` (`source_type`),
KEY `index_request_method` (`request_method`),
KEY `index_ip` (`ip`),
KEY `index_create_date` (`create_date`),
KEY `index_response_code` (`response_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表';
3.自定义注解类<SysLogAnnotation.java
>
package cn.keyidea.common.annotation;
import cn.keyidea.common.constant.ConstantsExpand;
import java.lang.annotation.*;
/**
* 自定义系统日志注解,实现日志记录功能
*
* @author qyd
* @date 2022-10-17
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLogAnnotation
{
/**
* 执行模块
*/
String module() default "";
/**
* 业务描述
*/
String serviceDesc() default "";
/**
* 业务来源,参见{@link ConstantsExpand.SourceType}
* 默认为:1-用户日志
*/
int sourceType() default 1;
/**
* 业务类型,参看{@link ConstantsExpand.ServiceType}
* 默认为:99-其他
*/
int serviceType() default 99;
/**
* 是否进行日志记录,0-不记录 1-记录(默认值为1)
* 参看{@link ConstantsExpand.SysLogFlag}
*/
int flag() default 1;
}
4.自定义注解处理类<SysLogAnnotationAspect.java
>
- 其中涉及统一返回封装类<
BaseRes.java
>和<BaseResponse.java
>,由于历史代码遗留问题,在此进行了兼容,前者与后者主要区别- 后者继承了HashMap中put方法,可以实现HashMap的自定义返回<k,v>属性值,不过也因此会造成在集成Knife4j下,前端无法准备理解正常的返回对象属性描述,不推荐使用;
- 前者定义了范式数据约束,因此在和Knife4j结合后,可以在前端进行完整的呈现返回实体的描述,推荐使用。
- 其余涉及如工具类<
JacksonUtils.java
>和<ShiroUtils.java
>等,见文末附录。
package cn.keyidea.common.annotation;
import cn.keyidea.common.bean.BaseRes;
import cn.keyidea.common.bean.BaseResponse;
import cn.keyidea.common.constant.ConstantsExpand;
import cn.keyidea.common.constant.StatusCode;
import cn.keyidea.common.util.JacksonUtils;
import cn.keyidea.common.util.ShiroUtils;
import cn.keyidea.sys.entity.SysLog;
import cn.keyidea.sys.entity.SysUser;
import cn.keyidea.sys.mapper.SysLogMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 系统日志切面类
* <p>
* 自定义日志标记类(参看{@link SysLogAnnotation})
*
* @author qyd
* @date 2022-10-19
*/
@Order(2)
@Aspect
@Component
public class SysLogAnnotationAspect
{
private final static Logger logger = LoggerFactory.getLogger(SysLogAnnotationAspect.class);
@Autowired
private SysLogMapper sysLogMapper;
/**
* 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
* 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
* 方式一:@annotation:当执行的方法上拥有指定的注解时生效(本类采用这)
* 方式二:execution:一般用于指定方法的执行
*/
@Pointcut("@annotation(cn.keyidea.common.annotation.SysLogAnnotation)")
private void controllerAspect()
{
}
/**
* 环绕方法,可自定义目标执行的时机
*
* @param pjp JoinPoint的子接口,添加了两个方法
* Object proceed() throws Throwable; 执行目标方法
* Object proceed(Object[] var1) throws Throwable; 传入的新的参数去执行目标方法
* @return 此方法需要返回值,返回值视为目标方法的返回值
* @throws Throwable
*/
@Around("controllerAspect()")
public Object around(ProceedingJoinPoint pjp) throws Throwable
{
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 获取请求方式
String requestMethod = request.getMethod();
// 获取登录用户账户;用户登陆时,从参数中获取userName
String userName = null;
// 参数列表
String payload = null;
// 获取系统ip
String ip = request.getRemoteAddr();
// 拦截的实体类,就是当前正在执行的controller
Object target = pjp.getTarget();
// 拦截的方法名称,当前正在执行的方法,示例:login
String methodName = pjp.getSignature().getName();
// 目标方法所属类的类名,示例:cn.keyidea.sys.controller.SysLoginController
String className = pjp.getSignature().getDeclaringTypeName();
// 目标方法所属类的简单类名,示例:SysLoginController
String simpleName = pjp.getSignature().getDeclaringType().getSimpleName();
// 拦截的参数类型
Signature sig = pjp.getSignature();
// 获取参数列表
Map<String, Object> paramsMap = getParams(pjp);
// 方法通知前获取时间,为什么要记录这个时间呢?当然是用来计算模块执行时间的
long start = System.currentTimeMillis();
if (!(sig instanceof MethodSignature))
{
throw new IllegalArgumentException("该注解只能用于方法");
}
Class<?>[] parameterTypes = ((MethodSignature) sig).getMethod().getParameterTypes();
// 获得被拦截的方法
Method method = null;
try
{
method = target.getClass().getMethod(methodName, parameterTypes);
}
catch (NoSuchMethodException e1)
{
e1.printStackTrace();
}
catch (SecurityException e1)
{
e1.printStackTrace();
}
if (null == method)
{
return pjp.proceed();
}
// 判断是否包含自定义的注解
if (!method.isAnnotationPresent(SysLogAnnotation.class))
{
return pjp.proceed();
}
// 获取自定义注解中的内容
SysLogAnnotation sysLogAnnotation = method.getAnnotation(SysLogAnnotation.class);
String module = sysLogAnnotation.module();
String serviceDesc = sysLogAnnotation.serviceDesc();
int sourceType = sysLogAnnotation.sourceType();
int serviceType = sysLogAnnotation.serviceType();
int flag = sysLogAnnotation.flag();
// 根据业务来源,设置:1)用户名,2)参数列表(用户登陆时重新获取)
switch (sourceType)
{
case ConstantsExpand.SourceType.INSIDE_SYS:
case ConstantsExpand.SourceType.INSIDE_USER:
// 如果是用户登录进行特殊处理,1)用户名从参数中获取(由于此刻用户尚未登录,Shiro尚未管理到用户),2)用户密码脱敏
if (serviceType == ConstantsExpand.ServiceType.LOGIN && paramsMap.containsKey("sysUser"))
{
SysUser sysUser = (SysUser) paramsMap.get("sysUser");
userName = sysUser.getLoginName();
SysUser entity = new SysUser();
entity.setLoginName(sysUser.getLoginName());
entity.setPassword("xxx");
payload = entity.toString();
}
else
{
if (ShiroUtils.getUserEntity() != null)
{
userName = ShiroUtils.getUserEntity().getLoginName();
}
}
break;
default:
userName = "第三方请求";
}
// 封装日志实体对象
SysLog sysLog = new SysLog();
sysLog.setUserName(userName);
sysLog.setRequestMethod(requestMethod);
sysLog.setClassName(className);
sysLog.setMethodName(methodName);
if (paramsMap.containsKey("response"))
{
paramsMap.remove("response");
}
sysLog.setPayload(payload == null ? JacksonUtils.toJson(paramsMap) : payload);
sysLog.setCreateDate(new Date());
sysLog.setIp(ip);
sysLog.setModule(module);
sysLog.setServiceDesc(serviceDesc);
sysLog.setServiceType(serviceType);
sysLog.setSourceType(sourceType);
// 默认执行成功
sysLog.setResponseCode(StatusCode.SUCCESS.getCodeValue());
sysLog.setDescInfo(StatusCode.SUCCESS.getMsg());
Object object = pjp.proceed();
long end = System.currentTimeMillis();
sysLog.setResponseTime((int) (end - start));
// 如果方法体返回的是BaseRes或者BaseResponse,则获取其在失败后的自定义描述文字
if (object instanceof BaseRes)
{
BaseRes res = (BaseRes) object;
if (res.getCode() != StatusCode.SUCCESS.getCodeValue())
{
sysLog.setResponseCode(res.getCode());
sysLog.setDescInfo(res.getMsg());
}
}
else if (object instanceof BaseResponse)
{
BaseResponse res = (BaseResponse) object;
int code = (int) res.get("code");
String msg = (String) res.get("msg");
if (code != StatusCode.SUCCESS.getCodeValue())
{
sysLog.setResponseCode(code);
sysLog.setDescInfo(msg);
}
}
// 保存进数据库
logSave(sysLog, flag);
return object;
}
/**
* 进行日志保存
*
* @param log 日志实体类
* @param flag 参看{@link SysLogAnnotation#flag}
*/
private void logSave(SysLog log, int flag)
{
if (flag == 1)
{
sysLogMapper.insert(log);
}
else
{
logger.warn("日志不保存,进行打印:{}", log.toString());
}
}
/**
* 获取参数名和参数值
*
* @param pjp Proceedingjoinpoint继承自JoinPoint,在JoinPoint的基础上暴露出 proceed(), 这个方法是AOP代理链执行的方法
* @return 返回参数名与参数值
*/
private static Map<String, Object> getParams(ProceedingJoinPoint pjp)
{
Map<String, Object> map = new HashMap<String, Object>(32);
Object[] values = pjp.getArgs();
String[] parameterNames = ((CodeSignature) pjp.getSignature()).getParameterNames();
for (int i = 0; i < parameterNames.length; i++)
{
if (values[i] instanceof MultipartFile)
{
// 对上传的文件做单独处理,获取文件名称作为value
MultipartFile file = (MultipartFile) values[i];
map.put(parameterNames[i], file.getOriginalFilename());
}
else
{
map.put(parameterNames[i], values[i]);
}
}
return map;
}
}
说明
- 日志注解处理类中统一对传入的参数中包含response的进行了移除,原因在于保留此参数的引用对于日志审计毫无用处,因此移除
5.日志注解使用示例
这里以系统配置控制器<SysController.java
>为例进行示例,在需要进行记录的接口上添加自定义注解@SysLogAnnotation
,并进行参数描述即可。
package cn.keyidea.sys.controller;
import cn.keyidea.common.annotation.SysLogAnnotation;
import cn.keyidea.common.bean.BaseRes;
import cn.keyidea.common.config.InitConfigProperties;
import cn.keyidea.common.constant.ConstantsExpand;
import cn.keyidea.common.valid.GroupAdd;
import cn.keyidea.common.valid.GroupUpdate;
import cn.keyidea.sys.entity.SysConfig;
import cn.keyidea.sys.service.SysConfigService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 系统配置控制器
*
* @author qyd
* @date 2022-10-17
*/
@ApiSupport(order = 7)
@Tag(name = "7-系统配置", description = "系统配置")
@RestController
@RequestMapping("/sys/sysConfig")
public class SysConfigController
{
private final static Logger logger = LoggerFactory.getLogger(SysConfigController.class);
@Autowired
private SysConfigService sysConfigService;
@Autowired
private InitConfigProperties initConfigProperties;
@SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-分页查询", serviceType = ConstantsExpand.ServiceType.QUERY)
@ApiOperationSupport(author = "qyd", order = 1)
@Operation(summary = "分页查询", description = "")
@Parameters({
@Parameter(name = "sysConfigName", description = "配置名称,支持模糊查询", required = false, example = "场景1", in = ParameterIn.QUERY),
@Parameter(name = "sysConfigCode", description = "配置key值,支持模糊查询", required = false, example = "0", in = ParameterIn.QUERY),
@Parameter(name = "current", description = "当前页", required = true, example = "1", in = ParameterIn.QUERY),
@Parameter(name = "size", description = "分页数", required = true, example = "15", in = ParameterIn.QUERY)
})
@GetMapping("listPage")
public BaseRes<BaseRes.DataList<SysConfig>> listPage(@RequestParam(value = "sysConfigName", required = false) String sysConfigName,
@RequestParam(value = "sysConfigCode", required = false) String sysConfigCode,
@RequestParam(value = "current", required = true) Integer pageNumber,
@RequestParam(value = "size", required = true) Integer pageSize)
{
QueryWrapper<SysConfig> wrapper = new QueryWrapper<>();
wrapper.lambda().like(StringUtils.isNotBlank(sysConfigName), SysConfig::getSysConfigName, sysConfigName)
.like(StringUtils.isNotBlank(sysConfigCode), SysConfig::getSysConfigCode, sysConfigCode);
Page<SysConfig> page = new Page<>(pageNumber, pageSize);
IPage<SysConfig> iPage = sysConfigService.page(page, wrapper);
return BaseRes.list(iPage.getTotal(), iPage.getRecords());
}
@SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-配置新增", serviceType = ConstantsExpand.ServiceType.ADD)
@ApiOperationSupport(author = "qyd", order = 2, includeParameters = {
"sysConfig.sysConfigName",
"sysConfig.sysConfigCode",
"sysConfig.sysConfigValue",
"sysConfig.remark"
})
@Operation(summary = "配置新增", description = "")
@PostMapping("add")
public BaseRes add(@Validated(GroupAdd.class) @RequestBody SysConfig sysConfig)
{
return sysConfigService.add(sysConfig);
}
@SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-配置更新", serviceType = ConstantsExpand.ServiceType.UPDATE)
@ApiOperationSupport(author = "qyd", order = 3, includeParameters = {
"sysConfig.id",
"sysConfig.sysConfigName",
"sysConfig.sysConfigValue",
"sysConfig.remark"
})
@Operation(summary = "配置更新", description = "配置key值一旦设置便不可更改,除非删除")
@PutMapping("/update")
public BaseRes update(@Validated(GroupUpdate.class) @RequestBody SysConfig sysConfig)
{
return sysConfigService.update(sysConfig);
}
@SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-配置删除", serviceType = ConstantsExpand.ServiceType.DELETE)
@ApiOperationSupport(author = "qyd", order = 4)
@Operation(summary = "删除", description = "")
@Parameter(name = "id", description = "主键ID", required = true, example = "1", in = ParameterIn.PATH)
@DeleteMapping("/delete/{id}")
public BaseRes delete(@PathVariable(value = "id", required = true) Integer id)
{
return sysConfigService.delete(id);
}
@SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-配置列表", serviceType = ConstantsExpand.ServiceType.UPDATE)
@ApiOperationSupport(author = "qyd", order = 5, includeParameters = {
"sysConfig.sysConfigName",
"sysConfig.sysConfigCode"
})
@Operation(summary = "配置列表", description = "name与code均支持模糊查询")
@GetMapping("/listAll")
public BaseRes listAll(SysConfig sysConfig)
{
List<SysConfig> list = sysConfigService.list(Wrappers.<SysConfig>lambdaQuery()
.like(StringUtils.isNotBlank(sysConfig.getSysConfigName()), SysConfig::getSysConfigName, sysConfig.getSysConfigName())
.like(StringUtils.isNotBlank(sysConfig.getSysConfigCode()), SysConfig::getSysConfigCode, sysConfig.getSysConfigCode())
.orderByDesc(SysConfig::getId));
return BaseRes.successData(list);
}
}
附录
附录A<BaseRes.java
>
package cn.keyidea.common.bean;
import cn.keyidea.common.constant.StatusCode;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 通用响应封装,范式返回(Swagger要求)
*
* @author qyd
*/
@Data
public class BaseRes<T> implements Serializable
{
/**
* 错误码
*/
@Schema(name = "code", description = "错误码,当code为1000时返回正常,其余返回异常", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000")
public Integer code;
/**
* 错误提示信息
*/
@Schema(name = "msg", description = "错误提示信息,当code为非1000时返回提示信息")
public String msg;
/**
* 附加返回数据
*/
@Schema(name = "data", description = "附加返回数据,当code为1000时返回数据")
public T data;
public static class DataList<T>
{
/**
* 记录总数
*/
@Schema(name = "total", description = "记录总数")
public Integer total;
/**
* 数据列表
*/
@Schema(name = "list", description = "数据列表")
public T list;
public DataList(Integer total, T list)
{
this.total = total;
this.list = list;
}
}
/**
* 给ObjectMapper用的,代码中不要调用
*/
public BaseRes()
{
}
/**
* 自定义返回码和提示消息
*
* @param code 错误码
* @param msg 提示文字
*/
public BaseRes(int code, String msg)
{
this.code = code;
this.msg = msg;
}
public BaseRes(int code, String msg, T data)
{
this.code = code;
this.msg = msg;
this.data = data;
}
/**
* 返回成功,但是没有附加数据
*
* @return BaseRes对象
*/
public static BaseRes success()
{
return new BaseRes(StatusCode.SUCCESS.getCodeValue(), "请求成功");
}
/**
* 返回成功,但是没有附加数据
*
* @param msg 成功时返回自定义msg
* @return BaseRes对象
*/
public static BaseRes success(String msg)
{
return new BaseRes(StatusCode.SUCCESS.getCodeValue(), msg);
}
/**
* 返回成功,带附加数据
*
* @param data 附加数据
* @return BaseRes对象
*/
public static BaseRes successData(Object data)
{
BaseRes value = new BaseRes(StatusCode.SUCCESS.getCodeValue(), "请求成功");
value.data = data;
return value;
}
/**
* 返回参数无效响应
*
* @return BaseRes对象
*/
public static BaseRes invalidParam()
{
return new BaseRes(StatusCode.INVALID_PARAM.getCodeValue(), "参数无效");
}
/**
* 返回参数无效响应,自定义错误提示
*
* @param msg 提示文字
* @return BaseRes对象
*/
public static BaseRes invalidParam(String msg)
{
return new BaseRes(StatusCode.INVALID_PARAM.getCodeValue(), msg);
}
/**
* 返回系统忙无效响应
*
* @return BaseRes对象
*/
public static BaseRes systemBusy()
{
return new BaseRes(StatusCode.SYSTEM_BUSY.getCodeValue(), "系统忙");
}
/**
* 返回master key无效响应
*
* @return BaseRes对象
*/
public static BaseRes invalidMasterkey()
{
return new BaseRes(StatusCode.INVALID_MASTER_KEY.getCodeValue(), "没有接口访问权限");
}
/**
* 返回失败,附带说明
*
* @return BaseRes对象
*/
public static BaseRes fail(String msg)
{
return new BaseRes(StatusCode.FAILURE.getCodeValue(), msg);
}
/**
* 返回错误信息时,仍然返回数据
*
* @param data 数据集
* @param msg 错误信息
* @return BaseRes对象
*/
public static BaseRes failData(Object data, String msg)
{
return new BaseRes(StatusCode.FAILURE.getCodeValue(), msg, data);
}
/**
* 登录失效的错误
*
* @return BaseRes对象
*/
public static BaseRes invalidToken()
{
return new BaseRes(StatusCode.INVALID_TOKEN.getCodeValue(), "请先登录");
}
/**
* 检查响应处理是否成功
*
* @return 成功返回true,否则false
*/
@JsonIgnore
public boolean isSuccess()
{
return (this.code.equals(StatusCode.SUCCESS.getCodeValue()));
}
/**
* 返回分页列表数据
*
* @param total 记录总数
* @param list 列表数据
* @return rsp
*/
public static BaseRes list(long total, Object list)
{
DataList data = new DataList((int) total, list);
return BaseRes.successData(data);
}
}
附录B<BaseResponse.java
>
package cn.keyidea.common.bean;
import cn.keyidea.common.constant.StatusCode;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.HashMap;
/**
* 通用响应封装
*/
public class BaseResponse extends HashMap<String, Object> {
public static class DataList {
//记录总数
public Long total;
//数据列表
public Object list;
public DataList(long total, Object list) {
this.total = total;
this.list = list;
}
}
/**
* 给ObjectMapper用的,代码中不要调用
*/
public BaseResponse() {
}
/**
* 自定义返回码和提示消息
*
* @param code 错误码
* @param msg 提示文字
*/
public BaseResponse(int code, String msg) {
put("code", code);
put("msg", msg);
}
/**
* 返回成功,但是没有附加数据
*
* @return BaseResponse对象
*/
public static BaseResponse success() {
return new BaseResponse(StatusCode.SUCCESS.getCodeValue(), "请求成功");
}
/**
* 返回成功,带附加数据
*
* @param data 附加数据
* @return BaseResponse对象
*/
public static BaseResponse successData(Object data) {
BaseResponse value = new BaseResponse(StatusCode.SUCCESS.getCodeValue(), "请求成功");
value.put("data", data);
return value;
}
/**
* 返回参数无效响应
*
* @return BaseResponse对象
*/
public static BaseResponse invalidParam() {
return new BaseResponse(StatusCode.INVALID_PARAM.getCodeValue(), "参数无效");
}
/**
* 返回参数无效响应,自定义错误提示
*
* @param msg 提示文字
* @return BaseResponse对象
*/
public static BaseResponse invalidParam(String msg) {
return new BaseResponse(StatusCode.INVALID_PARAM.getCodeValue(), msg);
}
/**
* 返回系统忙无效响应
*
* @return BaseResponse对象
*/
public static BaseResponse systemBusy() {
return new BaseResponse(StatusCode.SYSTEM_BUSY.getCodeValue(), "系统忙");
}
/**
* 返回master key无效响应
*
* @return BaseResponse对象
*/
public static BaseResponse invalidMasterkey() {
return new BaseResponse(StatusCode.INVALID_MASTER_KEY.getCodeValue(), "没有接口访问权限");
}
/**
* 返回失败,附带说明
*
* @return BaseResponse对象
*/
public static BaseResponse fail(String msg) {
return new BaseResponse(StatusCode.FAILURE.getCodeValue(), msg);
}
/**
* 登录失效的错误
*
* @return BaseResponse对象
*/
public static BaseResponse invalidToken() {
return new BaseResponse(StatusCode.INVALID_TOKEN.getCodeValue(), "请先登录");
}
/**
* 返回分页列表数据
*
* @param total 记录总数
* @param list 列表数据
* @return rsp
*/
public static BaseResponse list(long total, Object list) {
DataList data = new DataList(total, list);
return BaseResponse.successData(data);
}
/**
* 检查响应处理是否成功
*
* @return 成功返回true,否则false
*/
@JsonIgnore
public boolean isSuccess() {
return (get("code").equals(StatusCode.SUCCESS.getCode()));
}
@Override
public BaseResponse put(String key, Object value) {
super.put(key, value);
return this;
}
}
附录C<StatusCode.java
>
package cn.keyidea.common.constant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 状态码枚举定义
*
* @author qyd
* @date 2022-10-13
*/
public enum StatusCode
{
SUCCESS(1000, "请求成功"),
INVALID_PARAM(1001, "非法字段"),
SYSTEM_BUSY(1002, "系统忙"),
INVALID_MASTER_KEY(1003, "无接口访问权限"),
FAILURE(1004, "请求失败"),
UNAUTHORIZED(1005, "未授权"),
INVALID_TOKEN(2001, "TOKEN失效"),
CONNECT_TIMED_OUT(3001, "请求超时"),
HTTP_REQ_ERROR(3002, "HTTP请求出错");
/**
* 错误码
*/
private final int code;
/**
* 错误描述信息
*/
private final String msg;
StatusCode(int code, String msg)
{
this.code = code;
this.msg = msg;
}
public String getMsg()
{
return this.msg;
}
public String getCode()
{
return this.code + "";
}
public int getCodeValue()
{
return this.code;
}
/**
* 转为Map集合数据
*
* @return 枚举对象Map集合
*/
public static Map<Integer, String> toMap()
{
Map<Integer, String> map = new HashMap<>(32);
for (StatusCode value : StatusCode.values())
{
map.put(value.getCodeValue(), value.getMsg());
}
return map;
}
/**
* 转为List集合数据
*
* @return 枚举对象List集合
*/
public static List<Map<String, String>> toList()
{
List<Map<String, String>> list = new ArrayList<>(32);
Map<String, String> map = null;
for (StatusCode item : StatusCode.values())
{
map = new HashMap<>();
map.put("code", item.getCode());
map.put("msg", item.getMsg());
list.add(map);
}
map = null;
return list;
}
}
附录D<JacksonUtils.java
>
package cn.keyidea.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Jackson工具类
*
* @author qyd
* @date 2022-10-14
*/
public class JacksonUtils
{
private static final ObjectMapper MAPPER;
static
{
MAPPER = new ObjectMapper();
// 忽略不存在的字段
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 设置日期格式化
MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
/**
* 将对象转换成json数据
*/
public static String toJson(Object o)
{
try
{
return MAPPER.writeValueAsString(o);
}
catch (JsonProcessingException e)
{
e.printStackTrace();
}
return null;
}
/**
* 将json数据转化成对象
*/
public static <T> T toEntity(String jsonData, Class<T> beanType)
{
try
{
return MAPPER.readValue(jsonData, beanType);
}
catch (JsonProcessingException e)
{
e.printStackTrace();
}
return null;
}
/**
* 将json转化成List
*/
public static <T> List<T> toList(String jsonData, Class<? extends List> collectionClass, Class<T> elementClass)
{
JavaType javaType = MAPPER.getTypeFactory().constructCollectionType(collectionClass, elementClass);
try
{
return MAPPER.readValue(jsonData, javaType);
}
catch (JsonProcessingException e)
{
e.printStackTrace();
}
return null;
}
/**
* 将json转化成Map
*/
public static <K, V> Map<K, V> toMap(String jsonData, Class<? extends Map> mapClass, Class<K> keyClass, Class<V> valueClass)
{
JavaType javaType = MAPPER.getTypeFactory().constructMapType(mapClass, keyClass, valueClass);
try
{
return MAPPER.readValue(jsonData, javaType);
}
catch (JsonProcessingException e)
{
e.printStackTrace();
}
return null;
}
/**
* 禁止调用无参构造
*/
private JacksonUtils() throws IllegalAccessException
{
throw new IllegalAccessException("Can't create an instance!");
}
/**
* 将JSON数据转换成列表
*/
public static <T> List<T> getJsonToList(String jsonData, Class<T> beanType)
{
try
{
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
return MAPPER.readValue(jsonData, javaType);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
/**
* 将JSON数据转换成Set集合
*/
public static <E> Set<E> getJsonToSet(String jsonData, Class<E> elementType)
{
try
{
JavaType javaType = MAPPER.getTypeFactory().constructCollectionType(Set.class, elementType);
return MAPPER.readValue(jsonData, javaType);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
/**
* 将JSON数据转换成Map集合
*/
public static <K, V> Map<K, V> getJsonToMap(String jsonData, Class<K> keyType, Class<V> valueType)
{
try
{
JavaType javaType = MAPPER.getTypeFactory().constructMapType(Map.class, keyType, valueType);
return MAPPER.readValue(jsonData, javaType);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
附录E<ShiroUtils.java
>
package cn.keyidea.common.util;
import cn.keyidea.common.config.BusinessException;
import cn.keyidea.sys.entity.SysUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;
/**
* Shiro工具类
*
* @author qyd
*/
@Component
public class ShiroUtils
{
public static Session getSession()
{
return SecurityUtils.getSubject().getSession();
}
public static Subject getSubject()
{
return SecurityUtils.getSubject();
}
public static SysUser getUserEntity()
{
return (SysUser) SecurityUtils.getSubject().getPrincipal();
}
public static Integer getUserId()
{
return getUserEntity().getId();
}
public static void setSessionAttribute(Object key, Object value)
{
getSession().setAttribute(key, value);
}
public static Object getSessionAttribute(Object key)
{
return getSession().getAttribute(key);
}
public static boolean isLogin()
{
return SecurityUtils.getSubject().getPrincipal() != null;
}
public static String getKaptcha(String key)
{
Object kaptcha = getSessionAttribute(key);
if (kaptcha == null)
{
throw new BusinessException("验证码已失效");
}
getSession().removeAttribute(key);
return kaptcha.toString();
}
}