您的当前位置:首页正文

【springboot进阶】基于starter项目构建(二)构建starter项目-web

2024-12-01 来源:个人技术集锦


这篇我们开始学习创建属于自己的 starter ,实现一些常用模块的封装和自动配置,模拟 spirngboot 的 starter 模式,看看怎么将项目构建为 web starter 

一、创建 web-spring-boot-starter 项目

一般官方的 starter 是以 spring-boot-starter-{模块名},所以我们这边自定义的时候,区分于官方的命令,将模块名放在前面。

我们还是以一个 springboot 项目的方式来创建,如下图。

 选择目前最新的3.0.0版本,下面的依赖不需要勾选,等下我们再添加。

二、添加 pom 文件依赖

先贴上 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 工具了

三、构建配置

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 的包才能够使用。

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,用于子类继续处理过滤的逻辑,如果不想请求链继续,就抛出异常即可。

过滤器已经处理并解决 输入流只能读取一次的问题,对于这个问题的处理,具体可查看这篇文章 

四、加载自动化配置

从 springboot 2.7 的时候,spring.factories 这种方式已经标记为过期的,所以从 springboot3 开始已经完全移除了。所以我们要创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。

 这里并没有将 web配置类 BackendWebMvcConfig 加入其中,因为可能每个业务系统对于 mvc 相关的配置都会有比较大的差异,所以相关的配置还是交由业务系统来处理。

五、打包

这时候执行 mvn package & mvn install ,这样就将这个 starter 安装到本地仓库中。

六、使用

可以看 gitee 仓库的 springboot-advance-demo 项目。

显示全文