Mall
1 MyBatis Generator (MBG)
1.1 配置文件
generatorConfig.xml
生成 pojo 时指定 useJSR310Types
以使用 LocalDateTime
等类型
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 引用外部属性文件 -->
<properties resource="generator.properties"/>
<context id="MySqlContext" targetRuntime="MyBatis3" defaultModelType="flat">
<!-- 设置数据库标识符的起始和结束分隔符 -->
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<!-- 设置生成的Java文件的编码 -->
<property name="javaFileEncoding" value="UTF-8"/>
<!-- 插件:为模型生成序列化方法 -->
<!-- <plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>-->
<plugin type="com.mole.mall.mbg.CustomSerializablePlugin"/>
<!-- 插件:为生成的Java模型创建一个toString方法 -->
<!-- <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>-->
<!-- 插件:为生成的Java模型增加equals和hashCode方法 -->
<!-- <plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"/>-->
<!-- 插件:为生成的Java模型添加Lombok注解 -->
<plugin type="com.mole.mall.mbg.LombokPlugin"/>
<!-- 插件:为生成的Java模型删除get/set方法 -->
<plugin type="com.mole.mall.mbg.RemoveMethodsPlugin"/>
<!-- 插件:生成mapper.xml时覆盖原文件 -->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>
<!-- 自定义注释生成器配置 -->
<commentGenerator type="com.mole.mall.mbg.CommentGenerator">
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="true"/>
<!-- 是否去除生成的注释中的日期 -->
<property name="suppressDate" value="true"/>
<!-- 是否添加备注注释 -->
<property name="addRemarkComments" value="true"/>
</commentGenerator>
<!-- 数据库连接配置 -->
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.connectionURL}"
userId="${jdbc.userId}"
password="${jdbc.password}">
<!-- 解决MySQL驱动升级到8.0后不生成指定数据库代码的问题 -->
<property name="nullCatalogMeansCurrent" value="true"/>
</jdbcConnection>
<javaTypeResolver>
<!-- 类型解析器 -->
<property name="forceBigDecimals" value="false"/>
<property name="useJSR310Types" value="true"/>
</javaTypeResolver>
<!-- Java模型生成器配置 -->
<javaModelGenerator targetPackage="com.mole.mall.mbg.pojo" targetProject="mall-mbg\src\main\java">
<!-- 是否让schema作为包后缀 -->
<property name="enableSubPackages" value="false"/>
<!-- 从数据库返回的值被清理前后的空格 -->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- SQL映射文件生成器配置 -->
<sqlMapGenerator targetPackage="mapper" targetProject="mall-mbg\src\main\resources">
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!-- Java客户端生成器配置 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.mole.mall.mbg.mapper"
targetProject="mall-mbg\src\main\java">
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<!-- 表配置,生成全部表的代码 -->
<table tableName="%">
<!-- 设置主键生成策略 -->
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
</context>
</generatorConfiguration>
generator.properties
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
jdbc.userId=root
jdbc.password=root
1.2 生成工具类
Generator
package com.mole.mall.mbg;
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 用于生成MBG的代码
* Created by macro on 2018/4/26.
* Modified by Cyanix-0721 on 2024/9/25.
*/
public class Generator {
public static void main(String[] args) throws Exception {
// MBG 执行过程中的警告信息
List<String> warnings = new ArrayList<>();
// 当生成的代码重复时,覆盖原代码
boolean overwrite = true;
// 读取我们的 MBG 配置文件
try (InputStream is = Generator.class.getClassLoader().getResourceAsStream("generatorConfig.xml")) {
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(is);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
// 创建 MBG MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
// 执行生成代码
myBatisGenerator.generate(null);
}
// 输出警告信息
warnings.forEach(System.out::println);
}
}
CommentGenerator
package com.mole.mall.mbg;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.CompilationUnit;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.internal.DefaultCommentGenerator;
import org.mybatis.generator.internal.util.StringUtility;
import java.util.Properties;
/**
* 自定义注释生成器
* Created by macro on 2018/4/26.
* Modified by Cyanix-0721 on 2024/9/25. */public class CommentGenerator extends DefaultCommentGenerator {
private boolean addRemarkComments = false;
private static final String EXAMPLE_SUFFIX = "Example";
private static final String MAPPER_SUFFIX = "Mapper";
private static final String API_MODEL_PROPERTY_FULL_CLASS_NAME = "io.swagger.v3.oas.annotations.media.Schema";
/**
* 设置用户配置的参数
*
* @param properties 用户配置的属性
*/
@Override
public void addConfigurationProperties(Properties properties) {
super.addConfigurationProperties(properties);
this.addRemarkComments = StringUtility.isTrue(properties.getProperty("addRemarkComments"));
}
/**
* 给字段添加注释
*
* @param field 字段
* @param introspectedTable 内省表
* @param introspectedColumn 内省列
*/
@Override
public void addFieldComment(Field field, IntrospectedTable introspectedTable,
IntrospectedColumn introspectedColumn) {
String remarks = introspectedColumn.getRemarks();
// 根据参数和备注信息判断是否添加 Swagger 注解信息
if (addRemarkComments && StringUtility.stringHasValue(remarks)) {
// 数据库中特殊字符需要转义
if (remarks.contains("\"")) {
remarks = remarks.replace("\"", "'");
}
// 给模型的字段添加 Swagger 注解
field.addJavaDocLine("@Schema(title = \"" + remarks + "\")");
}
}
/**
* 给模型的字段添加注释
*
* @param field 字段
* @param remarks 备注信息
*/
private void addFieldJavaDoc(Field field, String remarks) {
// 文档注释开始
field.addJavaDocLine("/**");
// 获取数据库字段的备注信息
String[] remarkLines = remarks.split(System.lineSeparator());
for (String remarkLine : remarkLines) {
field.addJavaDocLine(" * " + remarkLine);
}
addJavadocTag(field, false);
field.addJavaDocLine(" */");
}
/**
* 给 Java 文件添加注释
*
* @param compilationUnit 编译单元
*/
@Override
public void addJavaFileComment(CompilationUnit compilationUnit) {
super.addJavaFileComment(compilationUnit);
// 只在模型文件中添加 Swagger 注解类的导入,不在 Example 或 Mapper 文件中添加
if (! compilationUnit.getType().getFullyQualifiedName().contains(MAPPER_SUFFIX) &&
! compilationUnit.getType().getFullyQualifiedName().contains(EXAMPLE_SUFFIX)) {
compilationUnit.addImportedType(new FullyQualifiedJavaType(API_MODEL_PROPERTY_FULL_CLASS_NAME));
}
}
}
1.3 插件
LombokPlugin
Lombok 替代原生 get/set/equals 等方法
package com.mole.mall.mbg;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import java.util.List;
/**
* LombokPlugin 是一个自定义的 MyBatis Generator 插件,
* 用于在生成的模型类中添加 Lombok 注解 (@Data, @NoArgsConstructor, @AllArgsConstructor)。
*/
public class LombokPlugin extends PluginAdapter {
/**
* 验证插件配置。
*
* @param warnings 如果配置无效,添加警告信息。
* @return true 如果配置有效。
*/
@Override
public boolean validate(List<String> warnings) {
return true;
}
/**
* 在基础记录类中添加 Lombok 注解。
*
* @param topLevelClass 生成的基础记录类。
* @param introspectedTable 从数据库中内省的表。
* @return true 继续处理。
*/
@Override
public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
addLombokAnnotations(topLevelClass);
return true;
}
/**
* 在主键类中添加 Lombok 注解。
*
* @param topLevelClass 生成的主键类。
* @param introspectedTable 从数据库中内省的表。
* @return true 继续处理。
*/
@Override
public boolean modelPrimaryKeyClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
addLombokAnnotations(topLevelClass);
return true;
}
/**
* 在包含 BLOB 的记录类中添加 Lombok 注解。
*
* @param topLevelClass 生成的包含 BLOB 的记录类。
* @param introspectedTable 从数据库中内省的表。
* @return true 继续处理。
*/
@Override
public boolean modelRecordWithBLOBsClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
addLombokAnnotations(topLevelClass);
return true;
}
/**
* 为指定的类添加 Lombok 注解 (@Data, @NoArgsConstructor, @AllArgsConstructor)。
*
* @param topLevelClass 要添加注解的类。
*/
private void addLombokAnnotations(TopLevelClass topLevelClass) {
topLevelClass.addImportedType(new FullyQualifiedJavaType("lombok.Data"));
topLevelClass.addImportedType(new FullyQualifiedJavaType("lombok.NoArgsConstructor"));
topLevelClass.addImportedType(new FullyQualifiedJavaType("lombok.AllArgsConstructor"));
topLevelClass.addAnnotation("@Data");
topLevelClass.addAnnotation("@NoArgsConstructor");
topLevelClass.addAnnotation("@AllArgsConstructor");
}
}
RemoveMethodsPlugin
package com.mole.mall.mbg;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import java.util.List;
/**
* RemoveMethodsPlugin 是一个自定义的 MyBatis Generator 插件,
* 用于移除生成的 get 和 set 方法。
*/
public class RemoveMethodsPlugin extends PluginAdapter {
/**
* 验证插件配置。
*
* @param warnings 如果配置无效,添加警告信息。
* @return true 如果配置有效。
*/
@Override
public boolean validate(List<String> warnings) {
return true;
}
/**
* 移除基础记录类中的 get 和 set 方法。
*
* @param topLevelClass 生成的基础记录类。
* @param introspectedTable 从数据库中内省的表。
* @return true 继续处理。
*/
@Override
public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
removeGettersAndSetters(topLevelClass);
return true;
}
/**
* 移除主键类中的 get 和 set 方法。
*
* @param topLevelClass 生成的主键类。
* @param introspectedTable 从数据库中内省的表。
* @return true 继续处理。
*/
@Override
public boolean modelPrimaryKeyClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
removeGettersAndSetters(topLevelClass);
return true;
}
/**
* 移除包含 BLOB 的记录类中的 get 和 set 方法。
*
* @param topLevelClass 生成的包含 BLOB 的记录类。
* @param introspectedTable 从数据库中内省的表。
* @return true 继续处理。
*/
@Override
public boolean modelRecordWithBLOBsClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
removeGettersAndSetters(topLevelClass);
return true;
}
/**
* 移除指定类中的 get 和 set 方法。
*
* @param topLevelClass 要移除方法的类。
*/
private void removeGettersAndSetters(TopLevelClass topLevelClass) {
topLevelClass.getMethods().removeIf(method -> method.getName().startsWith("get") || method.getName().startsWith("set"));
}
}
CustomSerializablePlugin
serialVersionUID 字段添加 @Serial 注解
package com.mole.mall.mbg;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.plugins.SerializablePlugin;
/**
* 自定义插件,用于在生成的类中为 serialVersionUID 字段添加 @Serial 注解。
*/
public class CustomSerializablePlugin extends SerializablePlugin {
@Override
protected void makeSerializable(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
super.makeSerializable(topLevelClass, introspectedTable);
addSerialAnnotation(topLevelClass);
}
/**
* 为指定类中的 serialVersionUID 字段添加 @Serial 注解。
*
* @param topLevelClass 要添加注解的类。
*/
private void addSerialAnnotation(TopLevelClass topLevelClass) {
for (Field field : topLevelClass.getFields()) {
if ("serialVersionUID".equals(field.getName())) {
field.addAnnotation("@Serial");
topLevelClass.addImportedType(new FullyQualifiedJavaType("java.io.Serial"));
}
}
}
}
2 日志
2.1 WebLogAspect.java
通过 AOP 实现记录 Controller 操作记录, 通过 Logstash 传输到 Elasticsearch
package com.mole.mall.common.log;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONUtil;
import com.mole.mall.common.domain.WebLog;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import net.logstash.logback.marker.Markers;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 统一日志处理切面
* Created by macro on 2018/4/26.
* Modified by Cyanix-0721 on 2024/09/28.
*/
@Aspect
@Component
@Order(1)
public class WebLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);
/**
* 定义切点,匹配所有控制器中的公共方法
*/
@Pointcut("execution(public * com.mole.mall.*.controller.*.*(..))")
public void webLog() {
}
/**
* 在切点方法执行前执行
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 目前不需要实现
}
/**
* 在切点方法返回后执行
*/
@AfterReturning(value = "webLog()", returning = "ret")
public void doAfterReturning(Object ret) throws Throwable {
// 目前不需要实现
}
/**
* 环绕切点方法执行
*
* @param joinPoint 连接点,表示被拦截的方法
* @return 方法执行结果
* @throws Throwable 如果方法执行过程中抛出异常
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 获取当前请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes != null ? attributes.getRequest() : null;
// 记录请求信息(通过Logstash传入Elasticsearch)
WebLog webLog = new WebLog();
Object result = joinPoint.proceed(); // 执行目标方法
Signature signature = joinPoint.getSignature(); // 获取方法签名
MethodSignature methodSignature = (MethodSignature) signature; // 强转为方法签名
Method method = methodSignature.getMethod(); // 获取方法对象
// 如果方法上有 @Operation 注解,获取其描述信息
if (method.isAnnotationPresent(Operation.class)) {
Operation log = method.getAnnotation(Operation.class);
webLog.setDescription(log.summary());
}
long endTime = System.currentTimeMillis();
String urlStr = request != null ? request.getRequestURL().toString() : null;
// 设置 WebLog 对象的属性
webLog.setBasePath(urlStr != null ? StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()) : null);
webLog.setIp(request != null ? request.getRemoteUser() : null);
webLog.setMethod(request != null ? request.getMethod() : null);
webLog.setParameter(getParameter(method, joinPoint.getArgs()));
webLog.setResult(result);
webLog.setSpendTime((int) (endTime - startTime));
webLog.setStartTime(startTime);
webLog.setUri(request != null ? request.getRequestURI() : null);
webLog.setUrl(request != null ? request.getRequestURL().toString() : null);
// 构建日志信息
Map<String, Object> logMap = new HashMap<>();
logMap.put("url", webLog.getUrl());
logMap.put("method", webLog.getMethod());
logMap.put("parameter", webLog.getParameter());
logMap.put("spendTime", webLog.getSpendTime());
logMap.put("description", webLog.getDescription());
// 记录日志信息
LOGGER.info(Markers.appendEntries(logMap), JSONUtil.parse(webLog).toString());
return result;
}
/**
* 根据方法和传入的参数获取请求参数
*
* @param method 方法对象
* @param args 方法参数
* @return 请求参数对象
*/
private Object getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
// 将 RequestBody 注解修饰的参数作为请求参数
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
// 将 RequestParam 注解修饰的参数作为请求参数
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>();
String key = parameters[i].getName();
if (StringUtils.hasLength(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.isEmpty()) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
}
2.2 WebLog.java
package com.mole.mall.common.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Controller层的日志封装类
* Created by macro on 2018/4/26.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class WebLog {
/**
* 操作描述
*/
private String description;
/**
* 操作用户
*/
private String username;
/**
* 操作时间
*/
private Long startTime;
/**
* 消耗时间
*/
private Integer spendTime;
/**
* 根路径
*/
private String basePath;
/**
* URI */ private String uri;
/**
* URL */ private String url;
/**
* 请求类型
*/
private String method;
/**
* IP地址
*/
private String ip;
/**
* 请求参数
*/
private Object parameter;
/**
* 返回结果
*/
private Object result;
}
2.3 logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>
<!-- 引用默认日志配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 使用默认的控制台日志输出实现 -->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- 应用名称 -->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="springBoot"/>
<!-- 日志文件保存路径 -->
<property name="LOG_FILE_PATH" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/logs}"/>
<!-- LogStash访问host -->
<springProperty name="LOG_STASH_HOST" scope="context" source="logstash.host" defaultValue="localhost"/>
<!-- 是否开启LogStash插件内部日志 -->
<springProperty name="ENABLE_INNER_LOG" scope="context" source="logstash.enableInnerLog" defaultValue="false"/>
<!-- 配置不打印logback内部的状态信息,排查logback配置问题时可注释掉 -->
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<!-- DEBUG日志输出到文件 -->
<appender name="FILE_DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 输出DEBUG以上级别日志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder>
<!-- 设置为默认的文件日志格式 -->
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 设置文件命名格式 -->
<fileNamePattern>${LOG_FILE_PATH}/debug/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!-- 设置日志文件大小,超过就重新生成文件,默认10M -->
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<!-- 日志文件保留天数,默认30天 -->
<maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>
<!-- ERROR日志输出到文件 -->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 只输出ERROR级别的日志 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<!-- 设置为默认的文件日志格式 -->
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 设置文件命名格式 -->
<fileNamePattern>${LOG_FILE_PATH}/error/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!-- 设置日志文件大小,超过就重新生成文件,默认10M -->
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<!-- 日志文件保留天数,默认30天 -->
<maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>
<!-- DEBUG日志输出到LogStash -->
<appender name="LOG_STASH_DEBUG" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<destination>${LOG_STASH_HOST}:4560</destination>
<addDefaultStatusListener>${ENABLE_INNER_LOG}</addDefaultStatusListener>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!-- 自定义日志输出格式 -->
<pattern>
<pattern>
{
"project": "mall",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<!-- ERROR日志输出到LogStash -->
<appender name="LOG_STASH_ERROR" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<destination>${LOG_STASH_HOST}:4561</destination>
<addDefaultStatusListener>${ENABLE_INNER_LOG}</addDefaultStatusListener>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!-- 自定义日志输出格式 -->
<pattern>
<pattern>
{
"project": "mall",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<!-- 业务日志输出到LogStash -->
<appender name="LOG_STASH_BUSINESS" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_STASH_HOST}:4562</destination>
<addDefaultStatusListener>${ENABLE_INNER_LOG}</addDefaultStatusListener>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!-- 自定义日志输出格式 -->
<pattern>
<pattern>
{
"project": "mall",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<!-- 接口访问记录日志输出到LogStash -->
<appender name="LOG_STASH_RECORD" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_STASH_HOST}:4563</destination>
<addDefaultStatusListener>${ENABLE_INNER_LOG}</addDefaultStatusListener>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!-- 自定义日志输出格式 -->
<pattern>
<pattern>
{
"project": "mall",
"level": "%level",
"service": "${APP_NAME:-}",
"class": "%logger",
"message": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<!-- 控制框架输出日志 -->
<logger name="org.slf4j" level="INFO"/>
<logger name="springfox" level="INFO"/>
<logger name="io.swagger" level="INFO"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.hibernate.validator" level="INFO"/>
<logger name="com.alibaba.nacos.client.naming" level="INFO"/>
<!-- 根日志记录器配置 -->
<root>
<level value="ERROR"/>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_DEBUG"/>
<appender-ref ref="FILE_ERROR"/>
<appender-ref ref="LOG_STASH_DEBUG"/>
<appender-ref ref="LOG_STASH_ERROR"/>
</root>
<!-- WebLogAspect类的日志配置 -->
<logger name="com.mole.mall.common.log.WebLogAspect" level="DEBUG">
<appender-ref ref="LOG_STASH_RECORD"/>
</logger>
<!-- com.mole.mall包的日志配置 -->
<logger name="com.mole.mall" level="DEBUG">
<appender-ref ref="LOG_STASH_BUSINESS"/>
</logger>
</configuration>
3 Redis
3.1 BaseRedisConfig.java
package com.mole.mall.common.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Redis基础配置
* Created by macro on 2020/6/19.
* Modified by Cyanix-0721 on 2024/9/28.
*/
public class BaseRedisConfig {
/**
* 配置 RedisTemplate
* * @param redisConnectionFactory Redis 连接工厂
* @return RedisTemplate 实例
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 创建一个 RedisSerializer 对象,用于序列化 Redis 中的值
RedisSerializer<Object> serializer = redisSerializer();
// 创建一个 RedisTemplate 对象,用于执行 Redis 操作
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 Redis 连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置键的序列化器为 StringRedisSerializer redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置值的序列化器为自定义的 JSON 序列化器
redisTemplate.setValueSerializer(serializer);
// 设置哈希键的序列化器为 StringRedisSerializer
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 设置哈希值的序列化器为自定义的 JSON 序列化器
redisTemplate.setHashValueSerializer(serializer);
// 在设置完所有属性后,调用 afterPropertiesSet 方法,确保所有属性都已设置
redisTemplate.afterPropertiesSet();
// 返回配置好的 RedisTemplate 实例
return redisTemplate;
}
/**
* 配置 Redis 序列化器
*
* @return RedisSerializer 实例
*/
@Bean
public RedisSerializer<Object> redisSerializer() {
// 创建 ObjectMapper 对象,用于配置 JSON 序列化
ObjectMapper objectMapper = new ObjectMapper();
// 设置所有访问权限的可见性
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 启用默认类型,必须设置,否则无法将 JSON 转化为对象,会转化成 Map 类型
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// 创建 JSON 序列化器, 将配置好的 ObjectMapper 设置到 JSON 序列化器中
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}
/**
* 配置 Redis 缓存管理器
*
* @param redisConnectionFactory Redis 连接工厂
* @return RedisCacheManager 实例
*/
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
// 创建一个 RedisCacheWriter 对象,不会锁定缓存
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// 配置 Redis 缓存,设置默认过期时间为 1 天
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer()))
.entryTtl(Duration.ofDays(1));
// 返回配置好的 RedisCacheManager 实例
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
}
3.2 RedisUtil.java
package com.mole.mall.admin.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 保存属性
*/
public void set(String key, Object value, long time) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
/**
* 保存属性
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 获取属性
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 删除属性
*/
public Boolean del(String key) {
return redisTemplate.delete(key);
}
/**
* 批量删除属性
*/
public Long del(List<String> keys) {
return redisTemplate.delete(keys);
}
/**
* 设置过期时间
*/
public Boolean expire(String key, long time) {
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
/**
* 获取过期时间
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断是否有该属性
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 按delta递增
*/
public Long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 按delta递减
*/
public Long decr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* 获取Hash结构中的属性
*/
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
/**
* 向Hash结构中放入一个属性
*/
public Boolean hSet(String key, String hashKey, Object value, long time) {
redisTemplate.opsForHash().put(key, hashKey, value);
return expire(key, time);
}
/**
* 向Hash结构中放入一个属性
*/
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
/**
* 直接获取整个Hash结构
*/
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 直接设置整个Hash结构
*/
public Boolean hSetAll(String key, Map<String, Object> map, long time) {
redisTemplate.opsForHash().putAll(key, map);
return expire(key, time);
}
/**
* 直接设置整个Hash结构
*/
public void hSetAll(String key, Map<String, ?> map) {
redisTemplate.opsForHash().putAll(key, map);
}
/**
* 删除Hash结构中的属性
*/
public void hDel(String key, Object… hashKey) {
redisTemplate.opsForHash().delete(key, hashKey);
}
/**
* 判断Hash结构中是否有该属性
*/
public Boolean hHasKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
/**
* Hash结构中属性递增
*/
public Long hIncr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, delta);
}
/**
* Hash结构中属性递减
*/
public Long hDecr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, -delta);
}
/**
* 获取Set结构
*/
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 向Set结构中添加属性
*/
public Long sAdd(String key, Object… values) {
return redisTemplate.opsForSet().add(key, values);
}
/**
* 向Set结构中添加属性
*/
public Long sAdd(String key, long time, Object… values) {
Long count = redisTemplate.opsForSet().add(key, values);
expire(key, time);
return count;
}
/**
* 是否为Set中的属性
*/
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 获取Set结构的长度
*/
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
}
/**
* 删除Set结构中的属性
*/
public Long sRemove(String key, Object… values) {
return redisTemplate.opsForSet().remove(key, values);
}
/**
* 获取List结构中的属性
*/
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 获取List结构的长度
*/
public Long lSize(String key) {
return redisTemplate.opsForList().size(key);
}
/**
* 根据索引获取List中的属性
*/
public Object lIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
/**
* 向List结构中添加属性
*/
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
}
/**
* 向List结构中添加属性
*/
public Long lPush(String key, Object value, long time) {
Long index = redisTemplate.opsForList().rightPush(key, value);
expire(key, time);
return index;
}
/**
* 向List结构中批量添加属性
*/
public Long lPushAll(String key, Object… values) {
return redisTemplate.opsForList().rightPushAll(key, values);
}
/**
* 向List结构中批量添加属性
*/
public Long lPushAll(String key, Long time, Object… values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
expire(key, time);
return count;
}
/**
* 从List结构中移除属性
*/
public Long lRemove(String key, long count, Object value) {
return redisTemplate.opsForList().remove(key, count, value);
}
}
4 Feign
4.1 FeignConfig.java
package com.mole.mall.demo.config;
import com.mole.mall.demo.component.FeignRequestInterceptor;
import feign.Logger;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Feign客户端的配置类
* Created by macro on 2019/9/5.
* Modified by Cyanix on 2024/09/29.
*/
@Configuration
public class FeignConfig {
/**
* 配置Feign的日志级别
*
* @return FULL日志级别
*/
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
/**
* 配置Feign请求拦截器
*
* @return FeignRequestInterceptor实例
*/
@Bean
RequestInterceptor requestInterceptor() {
return new FeignRequestInterceptor();
}
}
4.2 FeignRequestInterceptor.java
请求头可能会在以下情况下丢失:
跨服务调用:在微服务架构中,一个服务调用另一个服务时,原始请求头不会自动传递到下一个服务。例如,使用 Feign 客户端进行服务间调用时,默认情况下不会传递原始请求头。
负载均衡:在负载均衡器(如 Nginx 或 Spring Cloud Gateway)前面,负载均衡器可能不会将所有原始请求头传递给后端服务。
代理服务器:代理服务器(如 API 网关)可能会过滤或修改请求头,导致某些头信息丢失。
安全策略:某些安全策略或中间件可能会移除敏感的请求头信息以保护数据安全。
客户端限制:某些客户端(如浏览器或移动应用)可能会限制或移除某些请求头。
使用 FeignRequestInterceptor
可以确保在服务间调用时,重要的请求头信息(如认证令牌)不会丢失。
package com.mole.mall.demo.component;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Enumeration;
/**
* 用于Feign传递请求头的拦截器
* Created by macro on 2019/10/18.
* Modified by Cyanix-0721 on 2024/09/29.
*/
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 获取当前请求的属性
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
// 获取当前HTTP请求
HttpServletRequest request = attributes.getRequest();
// 获取请求中的所有头信息
Enumeration<String> headerNames = request.getHeaderNames();
// 在调用需要认证的内部接口时,需要获取原认证头并设置到requestTemplate中
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 传递content-length头会导致java.io.IOException: Incomplete output stream问题
if ("content-length".equalsIgnoreCase(name)) {
continue;
}
// 获取头的值
String values = request.getHeader(name);
// 将头信息设置到requestTemplate中
requestTemplate.header(name, values);
}
}
}
}
}
5 OpenAPI 3
- 使用
Knife4j
实现文档集成 - 通过
addSecur tyItem
, 给所有 http 接口添加 JWT 验证, HeaderAuthorization
, 前缀Bearer
package com.mole.mall.demo.config;
import com.mole.mall.common.constant.AuthConstant;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* SpringDoc相关配置
* Created by macro on 2024/3/5.
* Modify by Cyanix-0721 on 2024/9/29.
*/
@Configuration
public class SpringDocConfig implements WebMvcConfigurer {
private static final String SECURITY_SCHEME_NAME = AuthConstant.JWT_TOKEN_HEADER;
/**
* 配置OpenAPI的基本信息和安全设置
*
* @return OpenAPI实例
*/
@Bean
public OpenAPI mallAdminOpenAPI() {
return new OpenAPI()
.info(new Info().title("mall-demo系统")
.version("v1.0.0")
.license(new License().name("Apache 2.0")
.url("https://github.com/macrozheng/mall-learning")))
.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
.components(new Components()
.addSecuritySchemes(SECURITY_SCHEME_NAME,
new SecurityScheme()
.name(SECURITY_SCHEME_NAME)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer ")
.bearerFormat("JWT")));
}
/**
* 配置视图控制器,用于将`/swagger-ui/`路径重定向到`/swagger-ui/index.html`
* * @param registry
* 视图控制器注册表
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/swagger-ui/").setViewName("redirect:/swagger-ui/index.html");
}
/**
* 配置全局OpenApi自定义器,解决Knife4j配置认证后无法自动添加认证头的问题
*
* @return GlobalOpenApiCustomizer实例
*/
@Bean
public GlobalOpenApiCustomizer orderGlobalOpenApiCustomizer() {
return openApi -> {
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((s, pathItem) -> {
pathItem.readOperations().forEach(operation -> {
operation.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME));
});
});
}
};
}
}
6 spring-boot-starter-validation
spring-boot-starter-validation
中有 hibernate-validator
通过创建注解 @FlagValidator
实现对字段的合法性检测
package com.mole.mall.demo.validator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
/**
* 用户验证状态是否在指定范围内的注解
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = FlagValidatorClass.class)
public @interface FlagValidator {
// 指定允许的值范围
String[] value() default {};
// 验证失败时的错误消息
String message() default "flag is not found";
// 分组
Class<?>[] groups() default {};
// 负载
Class<? extends Payload>[] payload() default {};
}
package com.mole.mall.demo.validator;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
/**
* 状态标记校验器
*/
public class FlagValidatorClass implements ConstraintValidator<FlagValidator, Integer> {
private String[] values;
@Override
public void initialize(FlagValidator flagValidator) {
// 初始化允许的值范围
this.values = flagValidator.value();
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
// 验证值是否在允许的范围内
boolean isValid = false;
for (String s : values) {
if (s.equals(String.valueOf(value))) {
isValid = true;
break;
}
}
return isValid;
}
}
7 后端跨域处理
package com.mole.mall.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
@Configuration
public class GlobalCorsConfig {
/**
* 创建一个CORS过滤器Bean,允许所有来源、头和方法。
*
* @return 配置了指定CORS设置的CorsWebFilter。
*/
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*"); // 允许所有HTTP方法
config.addAllowedOriginPattern("*"); // 允许所有来源
config.addAllowedHeader("*"); // 允许所有头
config.setAllowCredentials(true); // 允许凭证(如Cookie、授权头等)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config); // 将CORS配置应用于所有路径
return new CorsWebFilter(source);
}
}
8 分页实现
使用 PageHelper 进行分页,服务调用 PageHelper 后直接返回列表,具体处理在控制器中返回封装过的分页类,利用了 ThreadLocal
9 返回结果封装/异常处理
ResultCode
javapackage com.mole.health.result; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor @AllArgsConstructor public enum ResultCode { SUCCESS(200, "操作成功"), FAILED(500, "操作失败"), VALIDATE_FAILED(404, "参数检验失败"), UNAUTHORIZED(401, "暂未登录或token已经过期"), FORBIDDEN(403, "没有相关权限"); private long code; private String message; }
CommonResult
javapackage com.mole.health.result; import cn.hutool.json.JSONUtil; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class CommonResult<T> { private long code; private String message; private T data; /** * 成功返回结果 * * @param data 获取的数据 */ public static <T> CommonResult<T> success(T data) { return new CommonResult<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } /** * 成功返回结果 * * @param data 获取的数据 * @param message 提示信息 */ public static <T> CommonResult<T> success(String message, T data) { return new CommonResult<>(ResultCode.SUCCESS.getCode(), message, data); } /** * 失败返回结果 * * @param errorCode 错误码 */ public static <T> CommonResult<T> failed(ResultCode errorCode) { return new CommonResult<>(errorCode.getCode(), errorCode.getMessage(), null); } /** * 失败返回结果 * * @param errorCode 错误码 * @param message 错误信息 */ public static <T> CommonResult<T> failed(ResultCode errorCode, String message) { return new CommonResult<>(errorCode.getCode(), message, null); } /** * 失败返回结果 * * @param message 提示信息 */ public static <T> CommonResult<T> failed(String message) { return new CommonResult<>(ResultCode.FAILED.getCode(), message, null); } /** * 失败返回结果 */ public static <T> CommonResult<T> failed() { return failed(ResultCode.FAILED); } /** * 参数验证失败返回结果 */ public static <T> CommonResult<T> validateFailed() { return failed(ResultCode.VALIDATE_FAILED); } /** * 参数验证失败返回结果 * * @param message 提示信息 */ public static <T> CommonResult<T> validateFailed(String message) { return failed(ResultCode.VALIDATE_FAILED, message); } /** * 未登录返回结果 */ public static <T> CommonResult<T> unauthorized() { return failed(ResultCode.UNAUTHORIZED); } /** * 未登录返回结果 */ public static <T> CommonResult<T> unauthorized(String message) { return failed(ResultCode.UNAUTHORIZED, message); } /** * 未授权返回结果 */ public static <T> CommonResult<T> forbidden() { return failed(ResultCode.FORBIDDEN); } /** * 未授权返回结果 */ public static <T> CommonResult<T> forbidden(String message) { return failed(ResultCode.FORBIDDEN, message); } @Override public String toString() { return JSONUtil.toJsonStr(this); } }
CommonPage
见上述[[mall #8 分页实现|分页实现]]ApiException
javapackage com.mole.health.exception; import com.mole.health.result.ResultCode; import lombok.Getter; @Getter public class ApiException extends RuntimeException { // 这个字段保存 ResultCode 实例,包含错误码和错误消息 private ResultCode errorCode; /** * 接受 ResultCode 的构造函数。 * 将 ResultCode 中的错误消息传递给 RuntimeException 的构造函数。 * 这样可以通过 getMessage() 方法获取错误消息。 * * @param errorCode 包含错误码和消息的 ResultCode 枚举实例 */ public ApiException(ResultCode errorCode) { super(errorCode.getMessage()); // 将消息传递给 RuntimeException this.errorCode = errorCode; // 存储 ResultCode 实例 } /** * 接受自定义错误消息的构造函数。 * 将自定义消息传递给 RuntimeException 的构造函数。 * * @param message 自定义错误消息 */ public ApiException(String message) { super(message); } /** * 接受 Throwable 原因的构造函数。 * 将原因传递给 RuntimeException 的构造函数。 * * @param cause 异常的原因 */ public ApiException(Throwable cause) { super(cause); } /** * 接受自定义错误消息和 Throwable 原因的构造函数。 * 将消息和原因传递给 RuntimeException 的构造函数。 * * @param message 自定义错误消息 * @param cause 异常的原因 */ public ApiException(String message, Throwable cause) { super(message, cause); } }
Asserts
javapackage com.mole.health.exception; import com.mole.health.result.ResultCode; public class Asserts { public static void fail(String message) { throw new ApiException(message); } public static void fail(ResultCode errorCode) { throw new ApiException(errorCode); } }
GlobalExceptionHandler
javapackage com.mole.health.exception; import com.mole.health.result.CommonResult; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice public class GlobalExceptionHandler { /** * 处理自定义API异常 * * @param e ApiException 异常实例 * @return 包含错误码或错误信息的 CommonResult */ @ResponseBody @ExceptionHandler(value = ApiException.class) public CommonResult<?> handle(ApiException e) { if (e.getErrorCode() != null) { return CommonResult.failed(e.getErrorCode()); } return CommonResult.failed(e.getMessage()); } /** * 处理方法参数验证异常 * * @param e MethodArgumentNotValidException 异常实例 * @return 包含验证错误信息的 CommonResult */ @ResponseBody @ExceptionHandler(value = MethodArgumentNotValidException.class) public CommonResult<?> handleValidException(MethodArgumentNotValidException e) { return getCommonResult(e.getBindingResult()); } /** * 处理绑定异常 * * @param e BindException 异常实例 * @return 包含验证错误信息的 CommonResult */ @ResponseBody @ExceptionHandler(value = BindException.class) public CommonResult<?> handleValidException(BindException e) { return getCommonResult(e.getBindingResult()); } /** * 获取通用结果 * * @param bindingResult 绑定结果 * @return 包含验证错误信息的 CommonResult */ private CommonResult<?> getCommonResult(BindingResult bindingResult) { String message = null; if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); if (fieldError != null) { message = fieldError.getField() + fieldError.getDefaultMessage(); } } return CommonResult.validateFailed(message); } }
WARNING
这个类 GlobalExceptionHandler
是一个全局异常处理类,它使用了 Spring 的 @ControllerAdvice
注解来捕获和处理应用程序中的异常。通过 @ExceptionHandler
注解,定义了几种常见的异常处理方法,以确保在出现异常时返回统一格式的响应。
工作流程
全局异常捕获
- 该类通过
@ControllerAdvice
注解成为一个全局异常处理器,当应用中的控制器方法抛出异常时,它会自动捕获并处理这些异常,提供统一的异常处理逻辑。
- 该类通过
处理自定义API异常
handle(ApiException e)
方法专门处理自定义的ApiException
。如果ApiException
包含错误码(ErrorCode
),返回带有错误码的CommonResult
,否则返回错误信息。
处理方法参数验证异常
handleValidException(MethodArgumentNotValidException e)
处理当请求参数校验失败时抛出的MethodArgumentNotValidException
异常。通过getCommonResult(e.getBindingResult())
获取绑定的结果(校验失败的信息),并返回包含错误信息的CommonResult
。
处理绑定异常
handleValidException(BindException e)
处理BindException
异常。类似于MethodArgumentNotValidException
,它通常也发生在参数校验过程中,通过getCommonResult
来返回验证失败的结果。
生成验证失败的响应
getCommonResult(BindingResult bindingResult)
方法根据BindingResult
获取字段的错误信息(如果有的话),然后构建并返回验证失败的CommonResult
。
MethodArgumentNotValidException
和 BindException
的关系与区别
这两个异常都与参数绑定和验证有关,但它们通常在不同的场景下抛出:
MethodArgumentNotValidException
- 这个异常通常发生在**
@RequestBody
** 注解处理的请求参数验证失败时。比如,当你接收到一个 JSON 请求,并且需要将其反序列化为某个对象,如果这个对象的字段不满足校验规则(如@NotNull
、@Size
等注解的校验),就会抛出MethodArgumentNotValidException
。 - 适用于请求体对象(通常是 JSON 请求)。
- 这个异常通常发生在**
BindException
BindException
则通常发生在**@ModelAttribute
** 或表单提交的数据绑定失败时。它也可以用于路径参数或查询参数的绑定验证失败场景。一般来说,这个异常发生在对普通的表单或 URL 参数进行数据绑定时。- 适用于表单提交、路径或查询参数。
总结
MethodArgumentNotValidException
用于处理@RequestBody
的 JSON 数据验证错误,而BindException
则更多用于表单或 URL 参数的数据绑定错误。- 在
GlobalExceptionHandler
中,这两个异常都通过BindingResult
来提取验证错误信息,统一返回CommonResult
格式的验证失败响应。