weblog项目开发记录--SpringBoot后端工程骨架
知识点查漏补缺
跟着犬小哈做项目实战时发现好多知识点都忘了,还有一些小的知识点可能之前没学过,记录下!顺带整理下开发流程。
完整项目学习见犬小哈实战专栏
SpringBoot后端工程骨架
搭建好的工程骨架中实现了很多基础功能,如日志配置、参数校验、自定义响应、全局异常管理、Knife4j、Jackson序列化配置等。熟悉这些功能组件可以再以后开发新的项目时作为模板显著提升开发效率!
1、多模块项目
1.1 parent标签
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.3</version><!-- Maven 从仓库中查找父项目--><relativePath/></parent>
parent 标签用于指定当前项目的父项目,这意味着当前项目会继承父项目的一些配置,例如插件版本、依赖版本、构建设置等。
1.2 modules标签
<!-- 子模块管理--><modules><!-- 入口模块--><module>weblog-web</module><!-- 管理后台 --><module>weblog-module-admin</module><!-- 通用模块 --><module>weblog-module-common</module></modules>
modules标签用来模块管理
1.3 properties标签
<properties><!-- 项目版本号--><revision>0.0.1-SNAPSHOT</revision><java.version>1.8</java.version><guava.version>31.1-jre</guava.version><commons-lang3.version>3.12.0</commons-lang3.version><jackson.verson>2.15.2</jackson.verson><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><!-- Maven 相关 --><maven.compiler.source>${java.version}</maven.compiler.source><maven.compiler.target>${java.version}</maven.compiler.target></properties>
properties标签中可以定义项目中可重用的属性或变量
1.4 dependencyManagement标签
<dependencyManagement><dependencies><dependency><groupId>com.bijing</groupId><artifactId>weblog-module-admin</artifactId><version>${revision}</version></dependency>......<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>${jackson.verson}</version></dependency></dependencies></dependencyManagement>
统一依赖管理,只是声明依赖,并不自动实现引入,只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项。
1.5 pluginManagement标签
<build><!-- 统一插件管理,用于管理 Maven 插件的版本和配置 --><pluginManagement><!-- 插件列表,包含了各个插件的配置 --><plugins><!-- 插件配置 --><plugin><!-- Spring Boot Maven 插件,用于构建和打包 Spring Boot 项目 --><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><!-- 插件的配置信息 --><configuration><!-- 配置选项,用于定制插件的行为 --><!-- 排除特定的依赖,这里是排除 lombok --><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></pluginManagement></build>
在父项目中声明插件的版本,以确保子项目使用相同的插件版本。
2、开发环境和生产环境配置
application.yml:默认的主配置文件,用于存放通用配置信息
# 企业级项目开发中,一般项目默认会激活 dev 环境
spring:profiles:#默认激活 dev 环境active: dev
application-dev.yml:针对开发环境的配置文件
application-prod.yml:针对生产环境的配置文件
# 在生产环境中使用特定的日志配置
logging:config: classpath:logback-weblog.xml
3、日志配置
在web模块的 pom.xml 中加入 spring-boot-starter-web 依赖时,它会自动包含 Logback 相关依赖,无需额外添加。日志功能一般放在common模块中,还需要加入下面依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- Jackson工具类 --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency>
3.1 logback日志
logback-weblog.xml配置文件如下(更多见博文):
<?xml version="1.0" encoding="UTF-8"?>
<configuration><!-- JMX配置,用于连接和管理JMX工具 --><jmxConfigurator/><!-- 引入Spring Boot默认日志配置 --><include resource="org/springframework/boot/logging/logback/defaults.xml" /><!-- 定义应用名称 --><property scope="context" name="appName" value="weblog" /><!-- 自定义日志输出路径,以及日志名称前缀 --><property name="LOG_FILE" value="../../logs/${appName}.%d{yyyy-MM-dd}"/><property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/><!-- 按照每天生成日志文件的配置 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 日志文件输出的文件名格式 --><FileNamePattern>${LOG_FILE}-%i.log</FileNamePattern><!-- 日志文件保留天数 --><MaxHistory>30</MaxHistory><!-- 日志文件最大的大小,当达到这个大小后,会触发滚动 --><TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>10MB</maxFileSize></TimeBasedFileNamingAndTriggeringPolicy></rollingPolicy><!-- 配置日志格式 --><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><pattern>${FILE_LOG_PATTERN}</pattern></encoder></appender><!-- dev环境配置,仅输出到控制台 --><springProfile name="dev"><include resource="org/springframework/boot/logging/logback/console-appender.xml" /><root level="info"><appender-ref ref="CONSOLE" /></root></springProfile><!-- prod环境配置,仅输出到文件中 --><springProfile name="prod"><include resource="org/springframework/boot/logging/logback/console-appender.xml" /><root level="INFO"><appender-ref ref="FILE" /></root></springProfile></configuration>
3.2 Spring Boot 自定义注解,实现 API 请求日志切面
3.2.1 自定义注解
一般四个步骤:
- 创建自定义注解: 这是定义自己的注解,可以在需要的地方标记,并可能带有一些属性。
package com.bijing.weblog.common.aspect;import java.lang.annotation.*;/*** @author 毕晶* @date 2024/2/3 20:50*/@Retention(RetentionPolicy.RUNTIME)//表示该注解在运行时保留,因此可以通过反射机制在运行时获取注解信息
@Target({ElementType.METHOD})//表示该注解仅能被应用在方法上。
@Documented//表示该注解将包含在 Javadoc 中
public @interface ApiOperationLog {/*** API功能描述* @return*/String description() default "";}
- 创建切面类(Aspect): 这是定义切面逻辑的地方。切面是使用注解的方法执行前后执行的代码块。
package com.bijing.weblog.common.aspect;import com.bijing.weblog.common.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;/*** @author 毕晶* @date 2024/2/3 21:02*/
@Aspect
@Component
@Slf4j
public class ApiOperationLogAspect {/*** 以自定义 @ApiOperationLog 注解为切点,凡是添加 @ApiOperationLog 的方法,都会执行环绕中的代码*/@Pointcut("@annotation(com.bijing.weblog.common.aspect.ApiOperationLog)")public void apiOperationLog() {}/*** 环绕** @param joinPoint* @return* @throws Throwable*/@Around("apiOperationLog()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {try {// 请求开始时间long startTime = System.currentTimeMillis();// MDC:诊断上下文映射,开发人员可以在 诊断上下文 中放置一些信息,如用户身份信息,关键参数,操作描述,环境信息,异常信息等,// 而后通过特定的 logback 组件去获取,或者MDC.get(key)MDC.put("traceId", UUID.randomUUID().toString());// 获取被请求的类和方法// joinPoint 包含了被拦截方法的信息,允许在拦截器中获取和控制被拦截方法的执行/*Signature: 通过 getSignature() 方法可以获取连接点的签名,即被拦截方法的方法签名。Args: 通过 getArgs() 方法可以获取方法的参数数组。Target Object: 通过 getTarget() 方法可以获取目标对象,即被拦截的对象实例。This Object: 通过 getThis() 方法可以获取代理对象,即当前执行的代理对象。*/String className = joinPoint.getTarget().getClass().getSimpleName();String methodName = joinPoint.getSignature().getName();// 请求入参Object[] args = joinPoint.getArgs();// 入参转 JSON 字符串// map(toJsonStr()):将流中的每个参数对象转换为相应的 JSON 字符串// collect(Collectors.joining(", ")):将流中的元素收集并连接成一个字符串,其中每个元素之间用逗号 , 分隔。String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", "));// 功能描述信息String description = getApiOperationLogDescription(joinPoint);// 打印请求相关参数log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} =================================== ",description, argsJsonStr, className, methodName);// 执行切点方法,result放的是被拦截方法的返回值Object result = joinPoint.proceed();// 执行耗时long executionTime = System.currentTimeMillis() - startTime;// 打印出参等相关信息log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} =================================== ",description, executionTime, JsonUtil.toJsonString(result));return result;} finally {MDC.clear();}}/*** 获取注解的描述信息** @param joinPoint* @return*/private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) {// 1. 从 ProceedingJoinPoint 获取 MethodSignature(方法签名信息)MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 2. 使用 MethodSignature 获取当前被注解的 MethodMethod method = signature.getMethod();// 3. 从 Method 中提取 LogExecution 注解ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);// 4. 从 LogExecution 注解中获取 description 属性return apiOperationLog.description();}/*** 转 JSON 字符串** @return*/private Function<Object, String> toJsonStr() {return JsonUtil::toJsonString;}
}
- 在启动类 WeblogWebApplication 中,手动添加包扫描 @ComponentScan: 在多模块项目中,Spring Boot 默认的组件扫描可能不会扫描所有模块的包,因此可能需要手动指定要扫描的包。
package com.bijing.weblog.web;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;@SpringBootApplication
@ComponentScan({"com.bijing.weblog.*"})// 多模块项目中,必需手动指定扫描 com.bijing.weblog 包下面的所有类
public class WeblogWebApplication {public static void main(String[] args) {SpringApplication.run(WeblogWebApplication.class, args);}}
- 在使用注解的方法上添加注解: 在需要应用切面逻辑的方法上添加自定义注解,这样 AOP 将在这些方法上生效。
补充:SpringBoot的AOP是默认开启的,不需要加注解@EnableAspectJAutoProxy
3.2.2 拾遗
元注解说明:
@Retention(RetentionPolicy.RUNTIME): 这个元注解用于指定注解的保留策略,即注解在何时生效。RetentionPolicy.RUNTIME 表示该注解将在运行时保留,这意味着它可以通过反射在运行时被访问和解析。
@Target({ElementType.METHOD}): 这个元注解用于指定注解的目标元素,即可以在哪些地方使用这个注解。ElementType.METHOD 表示该注解只能用于方法上。这意味着您只能在方法上使用这个特定的注解。
@Documented: 这个元注解用于指定被注解的元素是否会出现在生成的Java文档中。如果一个注解使用了 @Documented,那么在生成文档时,被注解的元素及其注解信息会被包含在文档中。这可以帮助文档生成工具(如 JavaDoc)在生成文档时展示关于注解的信息。
aspectj 注解说明
在配置 AOP 切面之前,我们需要了解下 aspectj 相关注解的作用:
@Aspect:声明该类为一个切面类;
@Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为切某个注解,也可以切某个 package 下的方法;
切点定义好后,就是围绕这个切点做文章了:
@Before: 在切点之前,织入相关代码;
@After: 在切点之后,织入相关代码;
@AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;
@AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;
@Around: 环绕,可以在切入点前后织入代码,并且可以自由的控制何时执行切点;
4、参数校验
Spring Boot提供了强大的参数校验功能,它建立在Java Bean Validation规范(JSR 380)之上。
4.1 引入依赖
首先,需要在 weblog-web 模块中的 pom.xml 文件添加参数校验依赖:
<!-- 参数校验依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
4.2 实体类参数校验
package com.bijing.weblog.web.model;import lombok.Data;import javax.validation.constraints.*;/*** @author 毕晶* @date 2024/2/3 23:40*/
@Data
public class User {// 用户名@NotBlank(message = "用户名不能为空")private String username;// 性别@NotNull(message = "性别不能为空")private Integer sex;// 年龄@NotNull(message = "年龄不能为空")@Min(value = 18, message = "年龄必须大于或等于 18")@Max(value = 120, message = "年龄必须小于或等于120")private Integer age;//邮箱@NotBlank(message = "邮箱不能为空")@Email(message = "邮箱格式不正确")private String email;
}
4.3 Controller 参数校验
每个字段的校验注解添加完成后,还需要在 controller 层进行捕获,并将错误信息返回。
package com.bijing.weblog.web.controller;import com.bijing.weblog.common.aspect.ApiOperationLog;
import com.bijing.weblog.web.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import java.util.stream.Collectors;/*** @author 毕晶* @date 2024/2/3 23:40*/
@RestController
@Slf4j
public class TestController {@PostMapping("/test")@ApiOperationLog(description = "测试接口")public ResponseEntity<String> test(@RequestBody @Validated User user, BindingResult bindingResult) {// 是否存在校验错误if (bindingResult.hasErrors()) {// 获取校验不通过字段的提示信息String errorMsg = bindingResult.getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(", "));return ResponseEntity.badRequest().body(errorMsg);}// 返参return ResponseEntity.ok("参数没有任何问题");}}
ResponseEntity: Spring Framework 提供的一个表示 HTTP 响应的类。它包装了响应的状态码、头部信息和响应体等信息。
@Validated: 告诉 Spring 需要对 User 对象执行校验;
BindingResult : 验证的结果对象,其中包含所有验证错误信息;
4.4 拾遗
1、以下是 JSR 380 中提供的主要验证注解及其描述:
注解 | 描述 |
---|---|
@NotNull | 验证对象值不应为 null。 |
@AssertTrue | 验证布尔值是否为 true。 |
@AssertFalse | 验证布尔值是否为 false。 |
@Min(value) | 验证数字是否不小于指定的最小值。 |
@Max(value) | 验证数字是否不大于指定的最大值。 |
@DecimalMin(value) | 验证数字值(可以是浮点数)是否不小于指定的最小值。 |
@DecimalMax(value) | 验证数字值(可以是浮点数)是否不大于指定的最大值。 |
@Positive | 验证数字值是否为正数。 |
@PositiveOrZero | 验证数字值是否为正数或零。 |
@Negative | 验证数字值是否为负数。 |
@NegativeOrZero | 验证数字值是否为负数或零。 |
@Size(min, max) | 验证元素的大小是否在给定的最小值和最大值之间。 |
@Digits(integer, fraction) | 验证数字是否在指定的位数范围内。 |
@Past | 验证日期或时间是否在当前时间之前。 |
@PastOrPresent | 验证日期或时间是否在当前时间或之前。 |
@Future | 验证日期或时间是否在当前时间之后。 |
@FutureOrPresent | 验证日期或时间是否在当前时间或之后。 |
@Pattern(regexp) | 验证字符串是否与给定的正则表达式匹配。 |
@NotEmpty | 验证元素不为 null,并且其大小/长度大于0。 |
@NotBlank | 验证字符串不为 null,且至少包含一个非空白字符。 |
@Email | 验证字符串是否符合有效的电子邮件格式。 |
除了上述的标准注解,JSR 380 也支持开发者定义和使用自己的自定义验证注解。此外,这个规范还提供了一系列的APIs和工具,用于执行验证和处理验证结果。大部分现代Java框架(如 Spring 和 Jakarta EE)都与 JSR 380 兼容,并支持其验证功能。
2、以下是 ResponseEntity 的一些主要用法:
方法 | 描述 |
---|---|
ok() | 创建一个状态码为 200 OK 的 ResponseEntity 对象。 |
ok(T body) | 创建一个状态码为 200 OK 的 ResponseEntity 对象,并设置响应体。 |
status(HttpStatus status) | 创建一个指定状态码的 ResponseEntity 对象。 |
status(int status) | 创建一个指定状态码的 ResponseEntity 对象。 |
headers(HttpHeaders headers) | 设置响应头部信息。 |
header(String headerName, String... headerValues) | 添加指定名称和值的响应头。 |
body(T body) | 设置响应体。 |
created(URI location) | 创建一个状态码为 201 Created 的 ResponseEntity 对象,并设置 Location 头部信息。 |
noContent() | 创建一个状态码为 204 No Content 的 ResponseEntity 对象。 |
4.5 查漏补缺
以下是 BindingResult 的一些主要用法和特点:
主要用法和特点 | 描述 |
---|---|
获取验证错误信息 | 通过 bindingResult.getFieldErrors() 方法可以获取所有验证失败的字段信息,每个字段错误包含字段名、错误码和默认错误信息等。 |
验证错误判断 | 使用 bindingResult.hasErrors() 方法来判断是否存在验证错误。如果存在验证错误,可以根据实际情况进行相应的处理。 |
默认错误信息 | 如果验证失败,BindingResult 会默认将错误信息存储在 FieldError 中,可以通过 getDefaultMessage() 方法获取默认的错误信息。 |
全局错误 | 除了字段级别的错误信息,BindingResult 还可以包含全局错误信息。通过 bindingResult.getGlobalErrors() 获取。 |
5、自定义响应工具类
在开发 RESTful API 时,为了保持响应结构的一致性,公司内部一般都有标准化的响应格式。
5.1 设计响应模型
5.1.1接口执行成功返参格式
{"success": true,"data": null
}
5.1.2接口执行异常返参格式
{"success": false,"errorCode": "10000""message": "用户名不能为空"
}
5.2创建响应参数工具类
可以在common模块的utils包中定义响应参数工具类:
package com.bijing.weblog.common.utils;import lombok.Data;import java.io.Serializable;/*** @author 毕晶* @date 2024/2/5 23:51*/
@Data
public class Response<T> implements Serializable {// 是否成功,默认为 trueprivate boolean success = true;// 响应消息private String message;// 异常码private String errorCode;// 响应数据private T data;// =================================== 成功响应 ===================================public static <T> Response<T> success() {Response<T> response = new Response<>();return response;}public static <T> Response<T> success(T data) {Response<T> response = new Response<>();response.setData(data);return response;}// =================================== 失败响应 ===================================public static <T> Response<T> fail() {Response<T> response = new Response<>();response.setSuccess(false);return response;}public static <T> Response<T> fail(String errorMessage) {Response<T> response = new Response<>();response.setSuccess(false);response.setMessage(errorMessage);return response;}public static <T> Response<T> fail(String errorCode, String errorMessage) {Response<T> response = new Response<>();response.setSuccess(false);response.setErrorCode(errorCode);response.setMessage(errorMessage);return response;}}
5.3 在控制器中使用
有了 Response 工具类,再配合 Spring Boot 的 @RestController 或者 @ResponseBody 注解, 就可以快速生成 JSON 格式的响应数据了。
@PostMapping("/test")@ApiOperationLog(description = "测试接口")public Response test(@RequestBody @Validated User user, BindingResult bindingResult) {// 是否存在校验错误if (bindingResult.hasErrors()) {// 获取校验不通过字段的提示信息String errorMsg = bindingResult.getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(", "));return Response.fail(errorMsg);}// 返参return Response.success();}
补充: 在接口的返参中,有很多 null 值的字段也返回了,咋办?
只需在 applicaiton.yml 文件中对 jackson 添加相关配置即可。
jackson:# 设置后台返参,若字段值为 null, 则不返回default-property-inclusion: non_null# 设置日期字段格式date-format: yyyy-MM-dd HH:mm:ss
6、全局异常管理
除了系统异常,很多时候我们还需要处理业务异常,比较推荐的做法是,将自定义业务异常整合到全局异常管理中,使其更加统一且易于维护。
6.1 自定义一个基础异常接口
创建一个 BaseExceptionInterface 基础异常接口,方便后面做拓展。
package com.bijing.weblog.common.exception;/*** @author 毕晶* @date 2024/2/6 15:04*/
public interface BaseExceptionInterface {String getErrorCode();String getErrorMessage();
}
6.2 自定义错误码枚举
package com.bijing.weblog.common.enums;import com.bijing.weblog.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;/*** @author 毕晶* @date 2024/2/6 15:05* @description 自定义错误码枚举*/
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {// ----------- 通用异常状态码 -----------SYSTEM_ERROR("10000", "出错啦,后台小哥正在努力修复中..."),// ----------- 业务异常状态码 -----------PRODUCT_NOT_FOUND("20000", "该产品不存在(测试使用)"),;// 异常码private final String errorCode;// 错误信息private final String errorMessage;}
6.3 自定义业务异常
package com.bijing.weblog.common.exception;import lombok.Getter;
import lombok.Setter;/*** @author 毕晶* @date 2024/2/6 15:10* @description 自定义业务异常*/
@Setter
@Getter
public class BizException extends RuntimeException {// 异常码private String errorCode;// 错误信息private String errorMessage;public BizException(BaseExceptionInterface baseExceptionInterface) {this.errorCode = baseExceptionInterface.getErrorCode();this.errorMessage = baseExceptionInterface.getErrorMessage();}
}
补充:为啥是继承RuntimeException类而不是去实现BaseExceptionInterface接口呢?
从设计的角度去考虑,BizException的本质是个运行时异常,实现BaseExceptionInterface接口是一种功能行为的实现,这一点在继承RuntimeException时可以自定义实现,因此就没有必要去实现BaseExceptionInterface接口了。当然,实现了BaseExceptionInterface接口代码上也没有啥影响。
6.4 参数校验异常
当捕获到MethodArgumentNotValidException异常后,我们可以通过全局异常处理器来捕获该异常,统一返回错误信息。
改造 GlobalExceptionHandler 类,添加 handleMethodArgumentNotValidException() 方法:
/*** 捕获参数校验异常* @return*/@ExceptionHandler({MethodArgumentNotValidException.class})@ResponseBodypublic Response<Object> handleMethodArgumentNotValidException(HttpServletRequest request,MethodArgumentNotValidException e) {// 参数错误异常码String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();// 获取 BindingResultBindingResult bindingResult = e.getBindingResult();StringBuilder sb = new StringBuilder();// 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com';Optional.ofNullable(bindingResult.getFieldErrors()).ifPresent(errors -> {errors.forEach(error ->sb.append(error.getField()).append(" ").append(error.getDefaultMessage()).append(", 当前值: '").append(error.getRejectedValue()).append("'; "));});// 错误信息String errorMessage = sb.toString();log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);return Response.fail(errorCode, errorMessage);}
在参数错误枚举中添加:
PARAM_NOT_VALID("10001", "参数错误"),
补充:在Controller中,需要不加 BindingResult 参数才能直接捕获其他异常并返回参数校验的异常信息。
@PostMapping("/test")@ApiOperationLog(description = "测试接口")
// 如果希望在发生验证错误时返回相应的错误信息,就需要加上 BindingResult 参数。
// 如果不需要处理验证错误,或者希望直接捕获其他异常并返回通用的错误信息,就可以不加 BindingResult 参数。public Response test(@RequestBody @Validated User user) {......}
7、整合 Knife4j
Knife4j 是一个为 Java 项目生成和管理 API 文档的工具。
7.1 整合 Knife4j
在父项目 weblog-springboot 中的 pom.xml 文件中,添加 Knife4j 依赖版本号:
<!-- 版本号统一管理 --><properties><!-- 依赖包版本 -->省略... <knife4j.version>4.3.0</knife4j.version></properties><!-- 统一依赖管理 --><dependencyManagement><dependencies>省略... <!-- knife4j(API 文档工具) --><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>${knife4j.version}</version></dependency></dependencies></dependencyManagement>
因为 admin 后台管理模块和博客前台模块都需要调试接口,所以,我们需要在 weblog-web 和 weblog-module-admin 两个模块中,都需要引入该依赖:
<!-- knife4j --><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId></dependency>
7.2 添加配置类
新建名为 Knife4jConfig 配置类:
package com.bijing.weblog.web.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;/*** @author 毕晶* @date 2024/2/7 16:22*/
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {@Bean("webApi")public Docket createApiDoc() {Docket docket = new Docket(DocumentationType.SWAGGER_2).apiInfo(buildApiInfo())// 分组名称.groupName("Web 前台接口").select()// 这里指定 Controller 扫描包路径.apis(RequestHandlerSelectors.basePackage("com.bijing.weblog.web.controller")).paths(PathSelectors.any()).build();return docket;}/*** 构建 API 信息** @return*/private ApiInfo buildApiInfo() {return new ApiInfoBuilder().title("Weblog 博客前台接口文档") // 标题.description("Weblog 是一款由 Spring Boot + Vue 3.2 + Vite 4.3 开发的前后端分离博客。") // 描述.termsOfServiceUrl("https://www.bilog.com/") // API 服务条款.contact(new Contact("bijing", "https://www.blog.com", "1457808125@qq.com")) // 联系人.version("1.0") // 版本号.build();}
}
浏览器访问路径 http://localhost:8080/doc.html , 就可以看到 api 管理界面了
7.3 给 controller 添加 Swagger 相关注解
7.4 生产环境如何屏蔽 Knife4j
7.4.1 Spring Boot Profile 特性
Profile 是 Spring Boot 中的一项特性,允许你在不同环境中使用不同的配置。
@Profile 注解:可以在配置类上添加 @Profile 注解,来控制 Knife4j 是否生效 。只有当指定的 Profile 处于激活状态时,该配置类才会被创建和被使用。
@Configuration
@EnableSwagger2WebMvc//启用 Swagger2
@Profile("dev")// 只在 dev 环境中开启
public class Knife4jConfig {
...
}
7.4.2 分组功能
weblog 项目接口分为前台和 Admin 后台,所以,除了在 weblog-web 模块中配置 Knife4j 外,还需要在 web-module-admin 也配置一份,并使用 Knife4j 分组功能将各自的接口隔离开来。
添加依赖、配置类和前面类似,注意类名,和 @Bean 的名称不能和 weblog-web 中的一样,否则会冲突。然后,改写分组名称,以及包扫描路径,还有 API 相关信息。
package com.bijing.weblog.admin.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;/*** @author 毕晶* @date 2024/2/20 13:42*/
@Configuration
@EnableSwagger2WebMvc
@Profile("dev")
public class Knife4jAdminConfig {@Bean("adminApi")public Docket createApiDoc() {Docket docket = new Docket(DocumentationType.SWAGGER_2).apiInfo(buildApiInfo())// 分组名称.groupName("Admin 后台接口").select()// 这里指定 Controller 扫描包路径.apis(RequestHandlerSelectors.basePackage("com.bijing.weblog.admin.controller")).paths(PathSelectors.any()).build();return docket;}/*** 构建 API 信息** @return*/private ApiInfo buildApiInfo() {return new ApiInfoBuilder().title("Weblog 博客前台接口文档") // 标题.description("Weblog 是一款由 Spring Boot + Vue 3.2 + Vite 4.3 开发的前后端分离博客。") // 描述.termsOfServiceUrl("https://www.bilog.com/") // API 服务条款.contact(new Contact("bijing", "https://www.blog.com", "1457808125@qq.com")) // 联系人.version("1.0") // 版本号.build();}
}
7.5 拾遗
常用的Swagger相关注解以及它们的作用:
注解 | 作用 |
---|---|
@Api | 描述整个API的信息,包括标题、描述等。 |
@ApiOperation | 描述单个接口的信息,包括接口的标题、描述、请求方法等。 |
@ApiParam | 描述接口的参数信息,包括参数名、描述、是否必需等。 |
@ApiModel | 描述请求或响应的模型信息,包括模型的名称、描述等。 |
@ApiModelProperty | 描述模型的属性信息,包括属性的名称、描述、是否必需等。 |
@ApiIgnore | 用于忽略某个接口或模型,不会被Swagger文档化。 |
@ApiImplicitParam | 描述接口的隐式参数信息,一般用于描述请求头信息。 |
@ApiImplicitParams | 描述接口的多个隐式参数信息。 |
@ApiResponses | 描述接口的响应信息,包括不同响应状态码对应的描述。 |
@ApiModelProperly | 描述模型的属性信息。 |
8、自定义 Jackson 序列化、反序列化,支持 Java 8 日期新特性
8.1 自定义 Jackson 配置类
由于 Spring Boot 内置使用的就是 Jackson JSON 框架,所以,无需引入新的依赖,仅需添加自定义配置类即可,让其支持新的日期 API。
package com.bijing.weblog.common.config;import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;/*** @author 毕晶* @date 2024/2/20 13:58*/
@Configuration
public class JacksonConfig {@Beanpublic ObjectMapper objectMapper() {// 初始化一个 ObjectMapper 对象,用于自定义 Jackson 的行为ObjectMapper objectMapper = new ObjectMapper();// 忽略未知字段(前端有传入某个字段,但是后端未定义接受该字段值,则一律忽略掉)objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);// JavaTimeModule 用于指定序列化和反序列化规则JavaTimeModule javaTimeModule = new JavaTimeModule();// 支持 LocalDateTime、LocalDate、LocalTime的序列化和反序列化//定义LocalDateTime的序列化器javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));//定义LocalDateTime的反序列化器javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy" +"-MM-dd")));javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm" +":ss")));objectMapper.registerModule(javaTimeModule);// 设置时区objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));// 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);return objectMapper;}
}
完成自定义 Jackson 配置类后可以在原先的weblog-web模块下application.yml中删去jackson的一些无用配置。
8.2 拾遗
在Java 8中,LocalDate、LocalTime 和 LocalDateTime 是处理日期和时间的重要类。
- LocalDate:
- LocalDate用于表示日期,不包含时间部分。
- 它只包含年、月、日三个字段。
- 例如,它可以用来表示员工的入职日期、某个活动的日期等。
- LocalDate对象通常用于需要日期信息但不涉及时间的场景。
- LocalTime:
- LocalTime用于表示时间,不包含日期部分。
- 它只包含时、分、秒三个字段,可以包含纳秒精度。
- 例如,它可以用来表示公交车的首发时间、会议的开始时间等。
- LocalTime对象适用于那些只需要时间信息,而日期信息不重要的场景。
- LocalDateTime:
- LocalDateTime是LocalDate和LocalTime的组合,它同时包含日期和时间。
- 它包含年、月、日、时、分、秒六个字段,也可以包含纳秒精度。
- LocalDateTime对象适用于需要同时记录日期和时间的场景,如在电商系统中记录交易发生的时间。
常用方法:
方法 | 描述 |
---|---|
LocalDate.now() | 获取当前日期 |
LocalDate.of(int year, int month, int dayOfMonth) | 创建特定日期 |
LocalTime.now() | 获取当前时间 |
LocalTime.of(int hour, int minute) 、LocalTime.of(int hour, int minute, int second) | 创建特定时间 |
LocalDateTime.now() | 获取当前日期时间 |
LocalDateTime.of(int year, int month, int dayOfMonth, int hour, int minute) 、LocalDateTime.of(int year, int month, int dayOfMonth, int hour, int minute, int second) | 创建特定日期时间 |
getYear() 、getMonthValue() 、getDayOfMonth() | 获取年、月、日 |
getHour() 、getMinute() 、getSecond() | 获取时、分、秒 |
plusDays(long daysToAdd) 、minusDays(long daysToSubtract) | 添加/减去指定天数 |
plusHours(long hoursToAdd) 、minusHours(long hoursToSubtract) | 添加/减去指定小时数 |
isEqual(LocalDate/LocalTime/LocalDateTime other) | 比较日期/时间/日期时间是否相等 |
isBefore(LocalDate/LocalTime/LocalDateTime other) 、isAfter(LocalDate/LocalTime/LocalDateTime other) | 比较日期/时间/日期时间的大小关系 |
相关文章:

weblog项目开发记录--SpringBoot后端工程骨架
知识点查漏补缺 跟着犬小哈做项目实战时发现好多知识点都忘了,还有一些小的知识点可能之前没学过,记录下!顺带整理下开发流程。 完整项目学习见犬小哈实战专栏 SpringBoot后端工程骨架 搭建好的工程骨架中实现了很多基础功能,…...

axios封装终极版实现token无感刷新及全局loading
前言 关于axios全局loading的封装博主已经发过一次了,这次是在其基础上增加了token的无感刷新。 token无感刷新流程 首次登录的时候会获取到两个token(AccessToken,RefreshToken)持久化保存起来(localStorage方案&a…...

推荐一个内网穿透工具,支持Windows桌面、Linux、Arm平台客户端
神卓互联是一款常用的内网穿透工具,它可以将本地服务器映射到公网上,并提供域名或子域名给外部访问。神卓互联具有简单易用、高速稳定的特点,支持Windows桌面版、Linux版、Arm版客户端,以及硬件等。 神卓互联内网穿透技术简介 企…...

【linux】vim多行操作命令
文章目录 1. vim多行同时修改2. vim复制/移动多行3. vim删除多行4. vim设置缩进空格 回顾:vi/vim常用命令 1. vim多行同时修改 (1) ctrl v (2) 按 下箭头,选择多行 (3) shift i,…...

vue-router钩子函数有哪些?都有哪些参数?
Vue.js是一款流行的JavaScript框架,它提供了大量的工具和特性,使得web前端开发更加高效和灵活。其中之一就是Vue-router,它是Vue.js官方路由插件,可以实现前端路由的管理和控制。在使用Vue-router时,我们可以利用钩子函…...

基于JavaWeb开发的小区车辆登记系统计算机毕设[附源码]
基于JavaWeb开发的小区车辆登记系统计算机毕设[附源码] 🍅 作者主页 央顺技术团队 🍅 欢迎点赞 👍 收藏 ⭐留言 📝 🍅 文末获取源码联系方式 📝 🍅 查看下方微信号获取联系方式 承接各种定制系统…...

【开源】SpringBoot框架开发高校宿舍调配管理系统
目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能需求2.1 学生端2.2 宿管2.3 老师端 三、系统展示四、核心代码4.1 查询单条个人习惯4.2 查询我的室友4.3 查询宿舍4.4 查询指定性别全部宿舍4.5 初次分配宿舍 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的…...

高压开关柜实现无线测温监测的关键点
一、概述 近年来,电厂自动化、信息化飞速发展,加快了对高压开关柜内的温度检测技术的研究。一系列的开关柜的无线测温监测技术也因此应运而生,并且发挥着越来越重要的作用。高压开关柜是发电厂、变电站、动力车间最重要的电气设备,…...

在线图片生成工具:定制化占位图片的利器
title: 在线图片生成工具:定制化占位图片的利器 date: 2024/2/20 14:08:16 updated: 2024/2/20 14:08:16 tags: 占位图片网页布局样式展示性能测试响应式设计在线生成开发工具 在现代的网页设计和开发中,占位图片扮演着重要的角色。占位图片是指在开发过…...

闭包----闭包的理解、优点
1、闭包的理解 闭包就是能够读取其他函数内部变量的函数。 由于在 javascript 中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理 解成 “ 定义在一个函数内部的函数 “ 。 所以,在本质上,闭包是将…...

jenkins的nmp install命令无法下载包
问题:在jenkin的流水线脚本中执行到:npm install命令后无法下载前端依赖包 1、进到jenkins的工作目录,一般在底层为/var/lib/jenkins/workspace/任务名称 cd /var/lib/jenkins/workspace/xkc处理方式: # 查看镜像源 npm config …...

Collection集合体系(ArrayList,LinekdList,HashSet,LinkedHashSet,TreeSet,Collections)
目录 一.Collection 二.List集合 三.ArrayList集合 四.LinkedList集合 五.Set集合 六.hashSet集合 七.LinkedHashSet集合 八.TreeSet集合 九.集合工具类Collections 集合体系概述 单列集合:Collection代表单列集合,每个元素&#…...

Job 和 DaemonSet
一、Job 1、Job 背景问题 K8s 里,最小的调度单元是 Pod,如果直接通过 Pod 来运行任务进程,会产生以下几种问题: ① 如何保证 Pod 内进程正确的结束? ② 如何保证进程运行失败后重试? ③ 如何管理多个任…...

C++ 二维前缀和 子矩阵的和
输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2 ,表示一个子矩阵的左上角坐标和右下角坐标。 对于每个询问输出子矩阵中所有数的和。 输入格式 第一行包含三个整数 n,m,q 。 接下…...

第六届计算机科学与技术在教育中的应用国际会议(CSTE 2024)
2024年第六届计算机科学与技术在教育中的应用国际会议(CSTE 2024)将于4月19-21日在中国西安举行。此次会议由陕西师范大学主办,陕西师范大学教育学部承办。在前五届成功举办的基础上,CSTE 2024将继续关注计算机科学与技术在教育领…...

Vue3学习——标签的ref属性
在HTML标签上,可以使用相同的ref名称,得到DOM元素ref放在组件上时,拿到的是组件实例(组件defineExpose暴露谁,ref才可以看到谁) <script setup lang"ts"> import RefPractice from /compo…...

数字化转型导师坚鹏:政府数字化转型之数字化技术
政府数字化转型之数字化技术 ——物联网、云计算、大数据、人工智能、虚拟现实、区块链、数字孪生、元宇宙等综合解析及应用 课程背景: 数字化背景下,很多政府存在以下问题: 不清楚新技术的发展现状? 不清楚新技术的重要应…...

go build
go build 作用:将Go语言程序和相关依赖编译成可执行文件 go build 无参数编译 生成当前目录名的可执行文件并放置于当前目录下,如: go build go build文件列表 编译同目录的多个源码文件时,可以在 go build 的后面提供多个文件…...

力扣238和169
一:238. 除自身以外数组的乘积 1.1题目 1.2思路 1.3代码 //左右乘表 int* productExceptSelf(int* nums, int numsSize, int* returnSize) {int* answer (int*)malloc(numsSize*sizeof(int));int i 0;int left[numsSize],right[numsSize];left[0] 1;for(i 1;…...

Android 基础技术——Framework
笔者希望做一个系列,整理 Android 基础技术,本章是关于 Framework 简述 Android 系统启动流程 当按电源键触发开机,首先会从 ROM 中预定义的地方加载引导程序 BootLoader 到 RAM 中,并执行 BootLoader 程序启动 Linux Kernel&…...

JavaWeb 中的静态资源访问
文章目录 JavaWeb 中的静态资源访问1. Tomcat 中的两个默认 ServletJSPServletDefaultServlet配置引起的 bug情况一情况二情况三 2. 总结3. 如何允许静态资源访问 JavaWeb 中的静态资源访问 1. Tomcat 中的两个默认 Servlet Tomcat 有两个默认的 Servlet,你的 Web…...

asp.net web api 用户身份验证
前后端分离的开发中,应用服务需要进行用户身份的验证才允许访问数据。实现的方法很简单。创建一个webapi项目。在App_Start目录下找到WebApiConfig.cs, 在里面增加一个实现类。 public static class WebApiConfig{public static void Register(HttpConfi…...

3DTile是不是没有坐标的选择?
可参考以下内容: 一、坐标参考系统(CRS) 3D Tiles 使用右手笛卡尔坐标系;也就是说,x和y的叉积产生z。3D Tiles 将z轴定义为局部笛卡尔坐标系的向上。tileset的全局坐标系通常位于WGS 84地心固定(ECEF)参考系(EPSG4978)中,但它不是必须的&am…...

数据采集三防平板丨三防平板电脑丨停车场应用
随着现代科技的不断发展,三防平板已经成为许多人工作和生活的必备工具。在停车场这个场景中,三防平板的应用可以大大提高停车场管理的效率和安全性。 停车场是现代城市交通管理的重要组成部分,它直接关系到城市交通的流畅和公共安全。停车场…...

解决git push时的too_many_commits提示
解决git push时的too_many_commits提示 提示内容 push时报错如下: Sorry, you were trying to upload xxxxxx commits in one push 原因分析 这个应该是因为在提交规则里配置了 一次只允许提交一个 commit,这样当 icode 上有 commit 没有合入时&…...

GPT-4助力我们突破思维定势
GPT-4在突破思维局限、激发灵感和促进知识交叉融合方面的作用不可小觑,它正逐渐成为一种有力的工具,助力各行业和研究领域的创新与发展。 GPT-4在突破传统思维模式、拓宽创新视野和促进跨学科知识融合方面扮演着越来越重要的角色: 突破思维…...

【前端工程化面试题】什么是 CI/CD
CI/CD 是软件开发中的两个重要实践,分别代表持续集成(Continuous Integration)和持续交付/持续部署(Continuous Delivery/Continuous Deployment)。 持续集成 (Continuous Integration, CI):持续集成是一种…...

kafka的安装,用于数据库同步数据
1.0 背景调研 因业务需求,需要查询其他部门的数据库数据,不方便直连数据库,所以要定时将他们的数据同步到我们的环境中,技术选型选中了kafkaCDC Kafka是Apache旗下的一款分布式流媒体平台,Kafka是一种高吞吐量、持久…...

Bean 的作用域你知道么 ?
Bean 的作用域有哪些? 所谓的作用域,其实就是说这个东西在哪个范围内可以被使用 , 如我们定义类的成员变量的时候使用的public,private等这些也是作用域的概念 Spring的Bean的作用域, 描述的就是这个Bean在哪个范围内可以被使用. 不同的作用域决定了Bean的创建, 管理和销毁的…...

Windows 使设置更改立即生效——并行发送广播消息
目录 前言 1 遍历窗口句柄列表 2 使用 SendMessageTimeout 发送延时消息 3 并行发送消息实现模拟广播消息 4 修改 UIPI 消息过滤器设置 5 托盘图标刷新的处理 6 完整代码和测试 本文属于原创文章,转载请注明出处: https://blog.csdn.net/qq_5907…...