目录
一、创建 web-spring-boot-starter 项目
二、添加 pom 文件依赖
三、构建配置
1. rest模板配置 RestTemplateConfig
2. 统一异常处理 BackendGlobalExceptionHandler
3. 统一返回数据结构
4. jwt鉴权处理
5. 请求日志切面处理 WebLogAspect
6. 邮件配置 BackendMailConfig
7. mvc 配置
四、加载自动化配置
五、打包
六、使用
这个系列讲解项目的构建方式,主要使用 父项目 parent 和 自定义 starter 结合。项目使用最新的 springboot3 和 jdk19。本系列的代码仓库看查看 gitee 仓库 的 starter 目录。
这篇我们开始学习创建属于自己的 starter ,实现一些常用模块的封装和自动配置,模拟 spirngboot 的 starter 模式,看看怎么将项目构建为 web starter
一、创建 web-spring-boot-starter 项目
一般官方的 starter 是以 spring-boot-starter-{模块名},所以我们这边自定义的时候,区分于官方的命令,将模块名放在前面。
我们还是以一个 springboot 项目的方式来创建,如下图。
选择目前最新的3.0.0版本,下面的依赖不需要勾选,等下我们再添加。
二、添加 pom 文件依赖
先贴上 pom.xml 代码,这里使用到上一章介绍的 backend-parent 父级项目作为这里的 parent
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.liurb.springboot.scaffold</groupId>
<artifactId>backend-parent</artifactId>
<version>1.0.0</version>
<relativePath />
</parent>
<artifactId>web-spring-boot-starter</artifactId>
<version>1.0.0</version>
<name>web-spring-boot-starter</name>
<description>web-spring-boot-starter</description>
<properties>
<common-spring-boot-starter.version>1.0.0</common-spring-boot-starter.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<exclusions>
<exclusion>
<artifactId>tomcat-embed-el</artifactId>
<groupId>org.apache.tomcat.embed</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.liurb.springboot.scaffold</groupId>
<artifactId>common-spring-boot-starter</artifactId>
<version>${common-spring-boot-starter.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>compile</scope>
</dependency>
<!--表示两个项目之间依赖不传递;不设置optional或者optional是false,表示传递依赖-->
<!--例如:project1依赖a.jar(optional=true),project2依赖project1,则project2不依赖a.jar-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>
</project>
依赖说明:
1)移除 spring-boot-starter-json :因为项目将使用 fastjson 或者 gson ,所以这里要移除springboot默认的 jackson 依赖。
2)okhttp:RestTemplate 请求模板使用 okhttp3
3)mail:邮件模块,主要用于系统异常的邮件通知功能
4)java-jwt:jwt模板,业务系统主要使用 jwt 鉴权
5)mapstruct:类复制工具,强烈推荐,不要再使用低效的 BeanUtils 工具了
三、构建配置
搭建好的 starter 目录与代码如下图,这个项目我们主要配置的是与 web 项目环境相关的内容,例如:rest请求模板的配置、统一异常处理、统一返回数据结构、jwt鉴权处理、请求返回日志打印、邮件发送配置、mvc相关配置等。
1. rest模板配置 RestTemplateConfig
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnProperty(
prefix = "web.starter.rest-template.config",
value = {"enabled"},
matchIfMissing = true)
@AutoConfiguration
public class RestTemplateConfig {
// todo ...
}
这里有一个配置开关,可以在配置文件中启用这个配置。我们一般在这个配置类设置一些请求超时时间、https证书问题的配置、日志打印拦截器等,具体可以查看源码。
2. 统一异常处理 BackendGlobalExceptionHandler
public abstract class BackendGlobalExceptionHandler {
@Resource
MailAccount mailAccount;
// todo ...
/**
* 邮件收件人列表
*
* @return
*/
public abstract List<String> tos();
/**
* 邮件标题
*
* @return
*/
public abstract String subject();
/**
* 是否发送邮件
*
* @return
*/
public abstract boolean isSendMail();
/**
* 发送邮件前处理
*
* @param requestUri
* @param errorMsg
* @return
*/
public abstract boolean sendMailBefore(String requestUri, String errorMsg);
/**
* 发送邮件后处理
*
* @param requestUri
* @param errorMsg
* @return
*/
public abstract void sendMailAfter(String requestUri, String errorMsg);
}
这是一个抽象类,并定义了几个与发邮件相关的抽象方法,让业务子类去实现相关的异常邮件配置。其中 发送邮件前处理 和 发送邮件后处理,这两个抽象方法主要用于限制发送异常邮件的次数,要不然邮箱就很容易满了。
3. 统一返回数据结构
这里主要由三个类实现:
- 统一结果返回实体 Result
@Data
public class Result<T> {
private Integer code;
private String msg;
private Boolean success;
private T data;
}
code为状态码,msg为消息,success为判断此次请求是否成功,T为返回的数据结果
- 统一返回信息枚举 ResultEnum
@Getter
@AllArgsConstructor
public enum ResultEnum {
/**
* 默认失败
*/
DEFAULT_FAIL(-99, "失败"),
/**
* 接口调用错误返回
*
*/
API_ERROR(-2,"接口调用错误"),
/**
* 系统错误返回
*
*/
SYS_ERROR(-1,"系统错误"),
/**
* 成功返回
*/
SUCCESS(0,"成功"),
;
final Integer code;
final String msg;
}
可以列举一些系统公共的状态码。
- 统一结果返回帮助类 ResultUtil
public class ResultUtil {
/**
* 成功返回
*
* @param object
* @return
*/
public static Result success(Object object){
Result result = new Result();
result.setSuccess(true);
result.setCode(ResultEnum.SUCCESS.getCode());
result.setMsg(ResultEnum.SUCCESS.getMsg());
result.setData(object);
return result;
}
public static Result success(Integer code, String msg, Object object){
Result result = new Result();
result.setSuccess(true);
result.setCode(code);
result.setMsg(msg);
result.setData(object);
return result;
}
/**
* 成功但不带数据
*
* @return
*/
public static Result success(){
return success(null);
}
/**
* 默认失败返回
*
* @param msg
* @return
*/
public static Result fail(String msg) {
Result result = new Result();
result.setSuccess(false);
result.setCode(ResultEnum.DEFAULT_FAIL.getCode());
result.setMsg(msg);
return result;
}
/**
* 失败返回
*
* @param code
* @param msg
* @return
*/
public static Result fail(Integer code, String msg){
Result result = new Result();
result.setSuccess(false);
if (null == code) {
code = ResultEnum.DEFAULT_FAIL.getCode();
}
result.setCode(code);
result.setMsg(msg);
return result;
}
}
定义请求 成功 和 失败 的返回信息结构。
具体的使用方法,可以查看这篇文章 接口的统一结果返回
4. jwt鉴权处理
使用 注解 + aop 的方式,对请求头的 token 信息鉴权并转为系统用户信息。
- 切入点注解 JwtUserAnnotation
- aop 处理 JwtUserAspect
- 系统用户信息类 JwtUser
具体的使用方法,可查看这篇文章aaa
5. 请求日志切面处理 WebLogAspect
使用 aop 方式,对 controller 控制器层的请求和返回分别处理日志的打印。
@Aspect
@Component
@Slf4j
@Order(10)
public class WebLogAspect {
/**
* 标记
*/
private String requestId;
/**
* 进入方法时间戳
*/
private Long startTime;
/**
* 方法结束时间戳(计时)
*/
private Long endTime;
public WebLogAspect() {
}
/**
* 定义请求日志切入点,其切入点表达式有多种匹配方式,这里是指定路径
*/
@Pointcut("execution(public * org.liurb..*.controller..*Controller.*(..))")
public void webLogPointcut() {
}
/**
* 前置通知:
* 1. 在执行目标方法之前执行,比如请求接口之前的登录验证;
* 2. 在前置通知中设置请求日志信息,如开始时间,请求参数,注解内容等
*
* @param joinPoint
* @throws Throwable
*/
@Before("webLogPointcut()")
public void doBefore(JoinPoint joinPoint) {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//创建标记
requestId = request.getHeader("backend-request-id");
if (StrUtil.isBlank(requestId)) {
requestId = StrUtil.uuid().replace("-","").toUpperCase();
}
//打印请求的内容
startTime = System.currentTimeMillis();
log.info("{} 请求Url : {}", requestId, request.getRequestURL().toString());
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
log.info("{} 请求UA : {}", requestId, userAgent);
log.info("{} 请求ip : {}", requestId, RequestHttpUtil.getIpAddress(request));
log.info("{} 请求方法 : {}", requestId, joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
log.info("{} 请求参数 : {}", requestId, Arrays.toString(joinPoint.getArgs()));
}
/**
* 返回通知:
* 1. 在目标方法正常结束之后执行
* 1. 在返回通知中补充请求日志信息,如返回时间,方法耗时,返回值,并且保存日志信息
*
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = "webLogPointcut()")
public void doAfterReturning(Object ret) {
endTime = System.currentTimeMillis();
log.info("{} 请求耗时:{}", requestId, (endTime - startTime) + "ms");
// 处理完请求,返回内容
log.info("{} 请求返回 : {}", requestId, ret);
}
/**
* 异常通知:
* 1. 在目标方法非正常结束,发生异常或者抛出异常时执行
* 1. 在异常通知中设置异常信息,并将其保存
*
* @param throwable
*/
@AfterThrowing(value = "webLogPointcut()", throwing = "throwable")
public void doAfterThrowing(Throwable throwable) {
// 打印异常日志记录
log.error("{} 抛出异常:{}", requestId, throwable.getMessage(), throwable);
}
}
doBefore 方法打印请求时的参数信息,也可以将IP、UA等信息打印出来。
doAfterReturning 方法打印返回时的数据信息,并记录这次请求的耗时。
doAfterThrowing 方法打印异常的情况信息。
6. 邮件配置 BackendMailConfig
邮件使用的是 hutool 的工具包,它是一个门面,具体的实现需要我们在项目中引入 javax.mail 的包才能够使用。
使用的时候只需要在项目的 resources 目录下创建 mail.setting 文件即可,具体的配置项可以参考项目代码。
7. mvc 配置
1)web配置类 BackendWebMvcConfig
public class BackendWebMvcConfig extends WebMvcConfigurationSupport {
/**
* 添加静态资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.addResourceLocations("classpath:/templates/")
.addResourceLocations("classpath:/META-INF/resources/");
}
/**
* 跨域支持
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
//对哪些目录可以跨域访问
registry.addMapping("/**")
//允许哪些网站可以跨域访问
.allowedOrigins("*")
//允许哪些方法
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
.maxAge(3600 * 24);
}
}
这是一个基类,继承 WebMvcConfigurationSupport ,重写了 添加静态资源方法 addResourceHandlers 和 跨域支持方法 addCorsMappings
2)公共过滤器 BackendHttpServletRequestFilter
public abstract class BackendHttpServletRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
try {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String contentType = httpRequest.getContentType();
if (StrUtil.isNotBlank(contentType) && contentType.contains(ContentType.MULTIPART.getValue())) {//不处理multipart/form-data类型的请求流复制
doFilterMore(request, response, chain);
chain.doFilter(request, responseWrapper);
} else {
CachedBodyHttpServletRequest cachedBodyHttpServletRequest =
new CachedBodyHttpServletRequest((HttpServletRequest) request);
doFilterMore(request, response, chain);
chain.doFilter(cachedBodyHttpServletRequest, responseWrapper);
}
} finally {
responseWrapper.copyBodyToResponse();
}
}
/**
* 提供一个方法可以对请求进行更多的过滤操作
*
* 返回false过滤拦截
*
* @param request
* @param response
* @param chain
*/
public abstract void doFilterMore(ServletRequest request, ServletResponse response, FilterChain chain);
}
定义为抽象类,并有一个抽象方法 doFilterMore,用于子类继续处理过滤的逻辑,如果不想请求链继续,就抛出异常即可。
过滤器已经处理并解决 输入流只能读取一次的问题,对于这个问题的处理,具体可查看这篇文章 HttpServletRequest输入流只能读取一次的问题
四、加载自动化配置
从 springboot 2.7 的时候,spring.factories 这种方式已经标记为过期的,所以从 springboot3 开始已经完全移除了。所以我们要创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。
这里并没有将 web配置类 BackendWebMvcConfig 加入其中,因为可能每个业务系统对于 mvc 相关的配置都会有比较大的差异,所以相关的配置还是交由业务系统来处理。
五、打包
这时候执行 mvn package & mvn install ,这样就将这个 starter 安装到本地仓库中。
六、使用
可以看 gitee 仓库的 springboot-advance-demo 项目。