SpringBoot 开发记录
SpringBoot 开发记录
本文基于 Spring Boot 3.x
统一响应体简单封装
为了方便前端对数据的处理,后端通常会统一返回给前端的响应体结构,这里我采用的 json 结构如下:
{
"success": true,
"code": 200,
"msg": "成功",
"data": {
"id": 1,
"username": "Trezedo"
}
}
其中 data
是一个泛型,可以根据业务需要调整。
下面给出根据我个人的使用习惯封装的响应体 Java 类:
import lombok.Getter;
import java.io.Serializable;
/**
* 响应体
*
* @author Trezedo
*/
@Getter
public class Result<T> implements Serializable {
/**
* 请求是否成功
*
* @implNote 准确来说应该用 successful
*/
private boolean success;
/**
* 状态码
*
* @apiNote 可以是 HTTP 状态码,或者是系统内部的状态码
*/
private int code;
/**
* 信息
*/
private String msg;
/**
* 数据
*/
private final T data;
public Result(T data) { this.data = data; }
/**
* @param data 数据
* @return 响应实体
*/
public static <T> Result<T> gen(T data) {
return new Result<>(data);
}
/**
* @return data 为 null 的响应实体
*/
public static <T> Result<T> gen() {
return gen(null);
}
/**
* @implNote of, ok, failed 方法仅用于修改除 data 外的字段
*/
public Result<T> of(boolean success, int code, String msg) {
this.success = success;
this.code = code;
this.msg = msg;
return this;
}
public Result<T> ok() { return of(true, 200, "成功"); }
public Result<T> ok(String msg) { return of(true, 200, msg); }
public Result<T> failed(String msg) { return of(false, 400, msg); }
public Result<T> failed(String msg, int code) { return of(false, code, msg); }
}
代码使用到 lombok 的
Getter
注解,当然也可以使用 IDEA 的快捷键 Alt+Insert 生成 Getter。
使用方法:先使用 gen
静态方法构造实体,之后再用 of
/ok
/failed
方法修改 success
, code
, msg
字段。
代码示例:
// 自动推断泛型
User user = getUserById(id);
Result.gen(user).ok("获取成功"); // Result<User>
// 不能自动推断时 T 为 Object
Result.gen().failed("用户不存在", 400); // Result<Object>
Result.gen(null).failed("用户不存在", 400); // Result<Object>
// 显式指定泛型
Result.<User>gen().failed("用户不存在", 400); // Result<User>
注意到上面的 gen
方法显式类型实参有时不能自动推断,需要手动指定。 这在控制器中可能不方便使用,因此下面再封装一个通用的 IBaseController
接口:
public interface IBaseController {
/**
* 请求成功
*
* @param data 数据内容
* @param <T> 对象泛型
* @return 响应结果
*/
default <T> Result<T> success(T data) {
return Result.gen(data).ok();
}
/**
* 请求失败
*
* @return 响应结果
*/
default <T> Result<T> failed() {
return Result.<T>gen().failed("请求失败");
}
/**
* 请求失败
*
* @param msg 提示内容
* @return 响应结果
*/
default <T> Result<T> failed(String msg) {
return Result.<T>gen().failed(msg);
}
/**
* 请求失败
*
* @param errorCode 状态码
* @param msg 提示内容
* @return 响应结果
*/
default <T> Result<T> failed(String msg, Integer errorCode) {
return Result.<T>gen().failed(msg, errorCode);
}
}
之所以封装成接口,是因为实际开发中可能会为了实现某种功能而让 Controller 继承一个抽象类,但在 Java 中一个类同时只能继承(extends)另一个类,而一个类可以同时实现(implements)多个接口。
我们可以有多种选择:
- 直接让业务中的 Controller 实现
IBaseController
接口;- 先定义抽象类 BaseController 并实现
IBaseController
接口,再让业务中的 Controller 继承 BaseController。
终止进程解决端口占用
当我们重启 IDEA 时,提示我们 “终止” 或 “断开连接”,如果选择后者,那么下次启动 SpringBoot 项目时就可能会有类似如下报错:
***************************
APPLICATION FAILED TO START
***************************
Description:
Web server failed to start. Port 62234 was already in use.
Action:
Identify and stop the process that's listening on port 62234 or configure this application to listen on another port.
这是因为之前关闭 IDEA 时的 “断开连接” 操作没有关闭运行在对应端口的进程,我们只需要手动关闭即可解决:
:: 查看哪个进程占用了 62234 端口
netstat -aon | findstr 62234
:: 强制杀死进程
taskkill -F -PID 17736
然后重启 SpringBoot Application 就可以了。
Mybatis Plus 多表条件查询
在 xxxMapper.xml
的 SQL 语句中使用 MybatisPlus 提供的 QueryWrapper 查询比纯写 xml 要方便。
类似如下需求,有学生(student)和专业(major)两个表:
create table major
(
`id` int auto_increment primary key,
`name` varchar(31) not null comment '专业名称',
`num` varchar(15) unique not null comment '专业编号',
) comment '专业';
create table student
(
`id` int auto_increment primary key,
`name` varchar(7) not null comment '姓名',
`num` varchar(15) not null comment '学号',
`major_id` int not null comment '专业',
foreign key (major_id) references major (id)
) comment '学生';
专业表的 id 是学生表的一个外键,传给前端时需要替换为专业名称。
定义 VO 层的学生对象,这里选择继承与数据库表对应的类:
import lombok.Data;
@Data
public class StudentVO extends Student {
/**
* 专业名称
*/
private String majorName;
}
定义 DAO 接口:
@Mapper
public interface StudentDAO extends BaseMapper<Student> {
List<StudentVO> listVO(@Param(Constants.WRAPPER) QueryWrapper<Student> query);
}
后续这部分将提取一个通用的接口。
这里 Constants.WRAPPER
的值为 "ew"
。接着在 xml 文件中编写 SQL,这里给出 2 种连接表的写法:
<!-- StudentMapper.xml -->
<select id="listVO" resultType="com.example.entity.vo.StudentVO">
select s.id, s.name, s.num, s.major_id, m.name as major_name
from student s
left join major m on s.major_id = m.id
<!-- 下面是 where 语句 -->
<if test="ew!=null and ew.customSqlSegment!=''">
${ew.customSqlSegment}
</if>
</select>
使用 left join
连接表;此处的 if
标签,一来可以避免 IDEA 对非标准 SQL 语法报错提示,二来能够避免查询条件为空导致语法错误。
<!-- StudentMapper.xml -->
<select id="listVO" resultType="com.example.entity.vo.StudentVO">
select s.id, s.name, s.num, s.major_id, m.name as major_name
from student s, major m
where s.major_id = m.id
<if test="ew!=null and ew.sqlSegment!=null and ew.sqlSegment != ''">
and ${ew.sqlSegment}
</if>
</select>
使用 where
连接表,当然条件语句也可以改用 <where>
标签,但不会有 SQL 代码提示。
最后给出具体用法,以下是测试程序:
@SpringBootTest
class ApplicationTests {
@Resource
StudentDAO studentDAO;
@Test
void q() {
QueryWrapper<Student> query = new QueryWrapper<>();
// 注意这里需要用表别名,因为两个表都有 name 列
// 表的别名在 SQL 中定义,一般取首字母
query.like("s.name", "秦");
List<StudentVO> list = studentDAO.listVO(query);
Optional.ofNullable(list).orElse(new ArrayList<>()).stream()
.filter(Objects::nonNull)
.forEach(System.out::println);
}
}
customSqlSegment
和 sqlSegment
的区别就是前者带 WHERE
。
当然,除了用 QueryWrapper 查询,还有分页的需求,这同样可以用 MybatisPlus 的分页插件实现,需要将接口的返回类型和参数作如下修改:
// 旧方式
List<StudentVO> getVOList(@Param(Constants.WRAPPER) QueryWrapper<Student> query);
// 新方式,增加 page 参数,修改返回类型
Page<StudentVO> pageVO(Page<StudentVO> page, @Param(Constants.WRAPPER) QueryWrapper<Student> query);
看着可能有些复杂,我们提取成一个通用的接口:
/**
* 自定义多表查询接口
*
* @param <T> 与数据库对应的 Java 类
* @author zedo
* @implNote 主要用于 POJO 转 VO,同时使用 MybatisPlus 的 QueryWrapper 做查询
*/
public interface IVOMapper<T> {
/**
* 分页查询
*
* @param page 分页对象
* @param query 查询条件
* @param <V> 视图层对象类型
* @return 分页查询结果
* @implNote 需要在对应的 Mapper.xml 中定义 id 为 pageVO 的 select 标签
*/
<V extends T> Page<V> pageVO(Page<V> page, @Param(Constants.WRAPPER) QueryWrapper<T> query);
}
这里特地用泛型 V
表示 VO 层的对象,泛型 T
也可以和 V
用同样的类。为了使用上面的接口,需要以下操作:
1,添加 xxxDAO
继承的接口:
@Mapper
public interface StudentDAO extends BaseMapper<Student>, IVOMapper<Student> {
}
2,编写对应的 xxxMapper.xml
,需要有一个 id="pageVO"
的 select
标签,就像上面方式 1 的 StudentMapper.xml
那样。
提示
为了让分页生效,记得配置分页插件:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
重要提示
需要注意的是,如果你使用的 JDK 版本在 9 及以上,构建运行前可能需要加上以下 VM 参数,否则执行会抛出异常:
--add-opens java.base/java.lang.invoke=ALL-UNNAMED
WebMvc 配置
配置跨域、拦截器、静态资源等,有以下方式实现:
- 实现
WebMvcConfigurer
接口(推荐)。 - 继承
WebMvcConfigurationSupport
类。
代码示例如下:
// 方式1
@Configuration
public class WebConfig implements WebMvcConfigurer {
}
// 方式2
@Configuration
public class WebConfig2 extends WebMvcConfigurationSupport {
}
注意同一个项目中不能同时使用多种方式配置!
如果你使用 IDEA,按下快捷键 Ctrl+O 打开重写函数面板,重写你需要的方法,例如:
- 配置跨域
addCorsMappings
- 添加拦截器
addInterceptors
- 静态资源处理
addResourceHandlers
跨域
除了可以在 Controller 上使用 @CrossOrigin
注解单独设置接口跨域,也可以统一配置:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
// .allowedMethods("*") // 允许所有方法
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
/**
* @param registry 资源处理注册中心
* @implNote 配置跨域和拦截器可能会限制静态资源的访问,因此需要实现 addResourceHandlers 方法,添加静态资源路径
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 通过 /static/** 访问静态资源
registry.addResourceHandler("static/**")
.addResourceLocations("classpath:/resources/")
.addResourceLocations("classpath:/static/");
}
}
自定义拦截器
身份验证、日志记录等场景可以使用拦截器。
以下是定义拦截器的方式:
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 目标方法执行前
// 如果不拦截直接 return true
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 目标方法执行后
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成时
}
}
如果想要拦截,建议抛出运行时异常(RuntimeException
),结合全局异常处理进行拦截,而不是 return false
。
添加到 MVC 配置中(示例):
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
MyInterceptor2 myInterceptor2;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截所有接口,登陆、注册、错误页除外
registry.addInterceptor(new MyInterceptor1())
.addPathPatterns("/**")
.excludePathPatterns("/auth/login", "/auth/register")
.excludePathPatterns("/error/**", "/static/**");
// 假设 myInterceptor2 注入了其他依赖
registry.addInterceptor(myInterceptor2);
// ...
}
}
默认情况下,拦截器会拦截所有的请求,除非在
addPathPatterns
方法中指定了需要拦截的路径(可多次调用)。 如果拦截器中需要注入 Redis 等,则注册拦截器时不能直接new
。
激活指定 profiles
springboot 默认使用 .properties
文件进行配置,我们也可以使用 .yml
(也即 .yaml
)。例如将 resources 目录下的的 application.properties
重命名为 application.yml
。
通常一个项目会区分不同的环境,每种环境使用不同的配置,例如开发环境下的数据库使用 localhost
,生产环境下使用某个公网 ip。
目录的结构如下:
application.yml
application-dev.yml
application-prod.yml
配置文件方式 1
最常见的做法是在 application.yml
添加以下内容来激活配置文件:
spring:
profiles:
active: dev # 激活 application-dev.yml
命令行方式
打包后,通过命令行指定激活配置文件有两种方式:
# 第 1 种
java -jar -Dspring.profiles.active=prod demo.jar
java -Dspring.profiles.active=prod -jar demo.jar
# 第 2 种
java -jar demo.jar --spring.profiles.active=prod
-D
方式设置 Java 系统属性要在-jar
之前定义或紧跟-jar
。如果需要激活多个 profile 可以使用逗号隔开,如:--spring.profiles.active=dev,test
环境变量方式
windows 系统使用 set
临时设置环境变量:
set SPRING_PROFILES_ACTIVE=dev,test
java -jar demo.jar
linux/mac 系统使用 export
:
export SPRING_PROFILES_ACTIVE=dev,test
java -jar demo.jar
配置文件方式 2
另外,我们也可以在 maven 打包时使用 -P
参数进行切换:mvn package -P prod
。
但是,更方便地,先在 pom.xml 定义 profiles:
<profiles>
<profile>
<id>dev</id>
<!-- 默认激活 -->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>
修改 application.yml
:
spring:
profiles:
active: "@spring.profiles.active@"
由于
application.properties
和application.yml
文件接受 Spring 样式的占位符 (${...}
),因此 Maven 过滤更改为使用@..@
占位符。Spring Boot Maven Plugin 这种方式虽然方便了一丢丢,但还是建议用环境变量来激活。
这样我们在打包时,在配置文件栏目可以选择需要激活的 profiles(多选),然后执行 package
即可:
提示
如果是开发过程中切换配置文件,需要手动点击一下刷新按钮:
避免我们重启 Application 时 profile 占位符 @spring.profiles.active@
没有被 Maven 替换。
以上几种方式的优先级:
命令行方式 > Java 系统属性方式 > 系统变量方式 > 配置文件方式