前言
本文将从最基础的Java SpringBoot环境,编写一个最基础的接口。
比在此基础上逐渐延申,最终形成一套标准的企业级后端接口开发规范。
1.环境搭建
1.1创建Maven工程
1.2引入所需依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qilixiang</groupId>
<artifactId>backend_api</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- SpringBoot Web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<!-- AspectJ支持-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<!-- Mybatis-Plus启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!-- mysql连接-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<!-- 自定义校验注解-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.16.Final</version>
<exclusions>
<exclusion>
<artifactId>validation-api</artifactId>
<groupId>javax.validation</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- swagger接口文档-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.3启动类
/**
* @author com.qilixiang
* @date 2022/12/26 15:50
*/
@SpringBootApplication
@Slf4j
@EnableScheduling
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1.4配置文件
server:
port: 8024
spring:
application:
name: api
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# 需要修改db连接
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
1.5测试接口
@RestController
@RequestMapping("user")
public class UserController {
@RequestMapping("/add")
public String add() {
return "SUCCESS";
}
}
目录结构
启动项目,请求接口测试
localhost:8024/user/add
2.统一返回数据格式
一个项目内的所有接口,必须有统一的风格,统一返回格式。
其中,返回需要包括最主要的三部分:状态码、信息、数据。状态码是其中最重要的,需要明确,且根据场景去分配。不妨可以参考下一些第三方的api,无一例外都是契合这个标准的。
构建接口返回结果类
// 默认code 1=成功、0=失败,这里不建议像这样写死状态码,应该用枚举维护。
@Data
public class Result<T> implements Serializable {
private int code = 1;
private String message;
private T data;
public Result() {
}
public Result(int code) {
this.code = code;
}
public Result(int code, String message) {
this.code = code;
this.message = message;
}
public Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Result<T> success(T data) {
Result result = new Result();
result.setData(data);
return result;
}
public static <T> Result<T> error(int code, String message) {
Result result = new Result(code, message);
return result;
}
public static <T> Result<T> error(String message) {
Result result = new Result(0, message);
return result;
}
}
改造测试接口
@PostMapping("/add")
public Result addUser() {
return Result.success("SUCCESS");
}
改造前
改造后
3.统一异常处理
为什么要配置统一异常处理
在用户体验方面来解释,代码中不可避免会触发一下bug,当bug发生时,给用户的反馈一般有以下两种情况。
对于用户来说,第一种反馈是十分不友好的,在用户的视角就是一堆乱码。第二种,起码能让用户知道,是服务器内部繁忙,就算其实不是。
配置全局异常处理
首先需要新建一个类,在这个类上加上 @ControllerAdvice/@RestControllerAdvice注解 , 这个类就配置成全局处理类了。然后在类中新建一系列方法,在方法上加上 @ExceptionHandler注解 并指定想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获全局所有异常
*/
@ExceptionHandler(value = Exception.class)
public Result exceptionHandler(Exception e) {
log.error("出现未知异常 -> ", e);
return Result.error(e.getMessage());
}
}
测试接口
@RequestMapping("/test")
public void test() {
int i = 1 / 0;
}
更多异常捕获
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获全局异常,处理所有不可知的异常
*/
@ExceptionHandler(value = Exception.class)
public Result exceptionHandler(Exception e) {
log.error("出现未知异常 -> ", e);
return Result.error(e.getMessage());
}
/**
* 捕获空指针
*/
@ExceptionHandler(value = NullPointerException.class)
public Result npeExceptionHandler(NullPointerException e) {
log.error("空指针 -> ", e);
return Result.error(e.getMessage());
}
/**
* 参数校验异常
*
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result ArgumentValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return Result.error(objectError.getDefaultMessage());
}
// 下面添加更多异常捕获
}
4.参数校验
任何、所有、每一个接口必须对参数进行安全校验!!
最简单常见做法,是把将逻辑写在业务代码层里。
/**
* 添加用户
*
* @param user 用户数据
* @return
*/
public boolean add(User user) {
if (user == null) {
log.info("对象不能为空");
return false;
}
if (StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
log.info("不能输入空字符串");
return false;
}
if (user.getPassword().length() < 8 || user.getPassword().length() > 12) {
log.info("密码长度必须是8-12个字符");
return false;
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
log.info("邮箱格式不正确");
return false;
}
// 参数校验通过,这里写上业务逻辑
return true;
}
这样做有以下不好的地方
- 代码冗长,业务不够清晰
- 后续还有很多校验,又得再写一套,复用性不高
Validator参数校验
在实体类(参数)内部定义校验规则+信息,接收到参数后自动根据事先定义好的规格,校验是否通过。
改造前
@Data
public class User {
private Long id;
private String username;
private String password;
private String email;
private Integer age;
}
改造后
@Data
public class User {
@NotNull(message = "用户id不能为空")
private Long id;
private String username;
@NotNull(message = "用户密码不能为空")
@Size(min = 8, max = 12, message = "密码长度必须是8-12个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
private Integer age;
}
在接口需要校验的参数上加上@Valid注解即可。因为已经提前配置了全局异常处理,校验失败的message也能有统一的格式返回。
@ApiOperation("添加用户")
@PostMapping("/add")
public Boolean addUser(@RequestBody @Valid User user) {
return userService.add(user);
}
测试
5.自定义异常
有时候,有一些异常是我们需要手动抛出的。比如在业务中,有些情况不符合业务逻辑,这时候可以选择手动抛出异常,中止继续操作,或回滚数据,其中,就可以加入我们自定义的异常。
我们现在就来开始写一个自定义异常
/**
* 自定义异常,需要继承runtimeException
*
* @author qilixiang
* @date 2022/12/26 14:15
*/
@Getter
public class CommonException extends RuntimeException {
private int code;
private String msg;
public CommonException() {
this(0, "接口异常");
}
public CommonException(String msg) {
this(0, msg);
}
public CommonException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
}
统一异常处理捕获此自定义异常
@ExceptionHandler(CommonException.class)
public Result APIExceptionHandler(CommonException e) {
return Result.error(e.getCode(), e.getMessage());
}
测试
@RequestMapping("/test")
public void test() {
throw new CommonException("自定义异常");
}
6.统一日志处理
SpringBoot已经对logback做了集成,只需简单配置即可。
logback-spring.xml(resource目录下)
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。
当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="10 seconds">
<contextName>logback</contextName>
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<property name="LOG_HOME" value="logs"/>
<property name="LOG_PATTERN"
value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!--1. 输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最低级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debug</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 2. 输出到文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 当前记录的日志文档完整路径 -->
<file>${LOG_HOME}/all.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5level ---[%15.15thread] %-40.40logger{39} : %msg%n%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数(非持续运行情况下,15天外不会清理)-->
<maxHistory>15</maxHistory>
<!-- 应对服务非持续偶尔运行,日志清理机制无法触发而导致日志得不到清理的情况-->
<totalSizeCap>500MB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
</appender>
<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、
以及指定<appender>。<logger>仅有一个name属性,
一个可选的level和一个可选的 addtivity 属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
如果未设置此属性,那么当前logger将会继承上级的级别。
addtivity:是否向上级logger传递打印信息。默认是true。
-->
<!--
root节点是必选节点,用来指定通用的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
不能设置为INHERITED或者同义词NULL。默认是DEBUG
可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-->
<!-- cloud环境下,去掉nacos的日志打印信息 -->
<logger name="com.alibaba.nacos" level="OFF" addtivity="false"></logger>
<logger name="com.qilixiang" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- 本地环境输出至控制台 -->
<!-- 如果使用了 springProfile, 需要将logback.xml名称改为logback-spring.xml-->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
</configuration>
配置文件
logging:
config: classpath:logback-spring.xml
会在项目根目录下生成日志文件夹
7.构建swagger接口文档
配置后开启即可
配置类
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value("${swagger.enable}")
private boolean enable;
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.enable(enable)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.qilixiang"))
.paths(PathSelectors.any())
.build();
}
/**
* 文档信息
*
* @return
*/
public ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("标题")
.description("描述")
.termsOfServiceUrl("qilixiang")
.contact(new Contact("联系人", "", "邮箱"))
.version("1.0")
.build();
}
}
application.yml
swagger:
enable: true #是否启用swagger
启动项目,访问 项目地址:端口/doc.html
最终的项目接口