这篇我们开始学习创建属于自己的 starter ,实现一些常用模块的封装和自动配置,模拟 spirngboot 的 starter 模式,看看怎么将项目构建为 web starter
一般官方的 starter 是以 spring-boot-starter-{模块名},所以我们这边自定义的时候,区分于官方的命令,将模块名放在前面。
我们还是以一个 springboot 项目的方式来创建,如下图。
选择目前最新的3.0.0版本,下面的依赖不需要勾选,等下我们再添加。
先贴上 pom.xml 代码,这里使用到上一章介绍的 父级项目作为这里的 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 工具了
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnProperty(
prefix = "web.starter.rest-template.config",
value = {"enabled"},
matchIfMissing = true)
@AutoConfiguration
public class RestTemplateConfig {
// todo ...
}
这里有一个配置开关,可以在配置文件中启用这个配置。我们一般在这个配置类设置一些请求超时时间、https证书问题的配置、日志打印拦截器等,具体可以查看源码。
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);
}
这是一个抽象类,并定义了几个与发邮件相关的抽象方法,让业务子类去实现相关的异常邮件配置。其中 发送邮件前处理 和 发送邮件后处理,这两个抽象方法主要用于限制发送异常邮件的次数,要不然邮箱就很容易满了。
这里主要由三个类实现:
@Data
public class Result<T> {
private Integer code;
private String msg;
private Boolean success;
private T data;
}
code为状态码,msg为消息,success为判断此次请求是否成功,T为返回的数据结果
@Getter
@AllArgsConstructor
public enum ResultEnum {
/**
* 默认失败
*/
DEFAULT_FAIL(-99, "失败"),
/**
* 接口调用错误返回
*
*/
API_ERROR(-2,"接口调用错误"),
/**
* 系统错误返回
*
*/
SYS_ERROR(-1,"系统错误"),
/**
* 成功返回
*/
SUCCESS(0,"成功"),
;
final Integer code;
final String msg;
}
可以列举一些系统公共的状态码。
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;
}
}
定义请求 成功 和 失败 的返回信息结构。
具体的使用方法,可以查看这篇文章
使用 注解 + aop 的方式,对请求头的 token 信息鉴权并转为系统用户信息。
具体的使用方法,可查看这篇文章aaa
使用 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 方法打印异常的情况信息。
邮件使用的是 hutool 的工具包,它是一个门面,具体的实现需要我们在项目中引入 javax.mail 的包才能够使用。
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,用于子类继续处理过滤的逻辑,如果不想请求链继续,就抛出异常即可。
过滤器已经处理并解决 输入流只能读取一次的问题,对于这个问题的处理,具体可查看这篇文章
从 springboot 2.7 的时候,spring.factories 这种方式已经标记为过期的,所以从 springboot3 开始已经完全移除了。所以我们要创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。
这里并没有将 web配置类 BackendWebMvcConfig 加入其中,因为可能每个业务系统对于 mvc 相关的配置都会有比较大的差异,所以相关的配置还是交由业务系统来处理。
这时候执行 mvn package & mvn install ,这样就将这个 starter 安装到本地仓库中。
可以看 gitee 仓库的 springboot-advance-demo 项目。