[springboot advanced] Construction based on the starter project (2) Build the starter project-web

Table of contents

1. Create a web-spring-boot-starter project

2. Add pom file dependencies

3. Build configuration

1. Rest template configuration RestTemplateConfig

2. Unified exception handling BackendGlobalExceptionHandler

3. Unified return data structure

4. jwt authentication processing

5. Request log aspect processing WebLogAspect

6. Mail configuration BackendMailConfig

7. mvc configuration

Fourth, load the automatic configuration

5. Packing

6. Use


This series explains how the project is built, mainly using the combination of the parent project parent and the custom starter. The project uses the latest springboot3 and jdk19. For the code repository of this series, see   the starter directory of the gitee repository .

In this article, we start to learn how to create our own starter, realize the encapsulation and automatic configuration of some common modules, simulate the starter mode of spirngboot, and see how to build the project as a web starter 

1. Create a web-spring-boot-starter project

Generally, the official starter is spring-boot-starter-{module name}, so when we customize it here, it is different from the official command and puts the module name in front.

We still create it as a springboot project, as shown below.

 Select the latest version 3.0.0, the following dependencies do not need to be checked, we will add them later.

2. Add pom file dependencies

Paste the pom.xml code first, and here use the backend-parent parent project introduced in the previous chapter   as the parent here

<?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>

Dependency description:

1) Remove spring-boot-starter-json: Because the project will use fastjson or gson, the default jackson dependency of springboot should be removed here.

2) okhttp: RestTemplate request template uses okhttp3 

3) mail: mail module, mainly used for system abnormal mail notification function

4) java-jwt: jwt template, the business system mainly uses jwt authentication

5) mapstruct: class copy tool, strongly recommended, don't use the inefficient BeanUtils tool anymore

3. Build configuration

The built starter directory and code are as shown in the figure below. We mainly configure the content related to the web project environment in this project, such as: configuration of rest request template, unified exception handling, unified return data structure, jwt authentication processing, request return log Printing, email sending configuration, mvc related configuration, etc.

1. Rest template configuration RestTemplateConfig

@ConditionalOnClass({RestTemplate.class})
@ConditionalOnProperty(
        prefix = "web.starter.rest-template.config",
        value = {"enabled"},
        matchIfMissing = true)
@AutoConfiguration
public class RestTemplateConfig {

    // todo ...    

}

Here is a config switch to enable this configuration in the config file. We generally set some request timeouts, configuration of https certificate issues, log printing interceptors, etc. in this configuration class. You can view the source code for details.

2. Unified exception handling 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);

}

This is an abstract class that defines several abstract methods related to sending emails, allowing business subclasses to implement related abnormal email configurations. Among them, pre-sending mail processing and post-sending mail processing, these two abstract methods are mainly used to limit the number of sending abnormal mails, otherwise the mailbox will be easily full.

3. Unified return data structure

This is mainly implemented by three classes:

  • Unified results return entity Result
@Data
public class Result<T> {

    private Integer code;
    private String msg;
    private Boolean success;
    private T data;

}

code is the status code, msg is the message, success is to judge whether the request is successful, and T is the returned data result 

  • Unified return information enumeration ResultEnum
@Getter
@AllArgsConstructor
public enum ResultEnum {

    /**
     * 默认失败
     */
    DEFAULT_FAIL(-99, "失败"),
    /**
     * 接口调用错误返回
     *
     */
    API_ERROR(-2,"接口调用错误"),
    /**
     * 系统错误返回
     *
     */
    SYS_ERROR(-1,"系统错误"),
    /**
     * 成功返回
     */
    SUCCESS(0,"成功"),
    ;
    
    final Integer code;
    final String msg;
}

 Some system common status codes can be listed.

  • Unified result return helper class 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;
    }


}

Define the return information structure of request success and failure.

For the specific usage method, you can check  the unified result return of the interface in this article

4. jwt authentication processing

Use annotation + aop to authenticate the token information in the request header and convert it into system user information.

  • Pointcut annotation JwtUserAnnotation
  • aop handles JwtUserAspect
  • System user information class JwtUser

For details on how to use it, see this article aaa

5. Request log aspect processing WebLogAspect

Using the aop method, the request and return to the controller controller layer process the printing of logs separately.

@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);
    }

}

The doBefore method prints the parameter information when requesting, and can also print out information such as IP and UA.

The doAfterReturning method prints the data information when returning, and records the time-consuming of this request.

The doAfterThrowing method prints information about the exception's condition.

6. Mail configuration BackendMailConfig

The mail uses the hutool toolkit, which is a facade. The specific implementation requires us to introduce the javax.mail package in the project before it can be used.

When using it, you only need to create the mail.setting file in the resources directory of the project. For specific configuration items, please refer to the project code.

7. mvc configuration

1) web configuration class 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);
    }

}

This is a base class that inherits from WebMvcConfigurationSupport and rewrites the method addResourceHandlers for adding static resources and the method addCorsMappings for cross-domain support

2) Public filter 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);

}

It is defined as an abstract class and has an abstract method doFilterMore, which is used for subclasses to continue processing the filtering logic. If you do not want the request chain to continue, just throw an exception.

The filter has processed and solved the problem that the input stream can only be read once. For the treatment of this problem, please refer to this article  HttpServletRequest The problem that the input stream can only be read once

Fourth, load the automatic configuration

From springboot 2.7, the spring.factories method has been marked as expired, so it has been completely removed from springboot3. So we have to create org.springframework.boot.autoconfigure.AutoConfiguration.imports file.

 The web configuration class BackendWebMvcConfig is not added here, because each business system may have a relatively large difference in the configuration related to mvc, so the relevant configuration is still handled by the business system.

5. Packing

At this time, execute mvn package & mvn install to install the starter into the local warehouse.

6. Use

You can see the springboot-advance-demo project in the gitee warehouse.

Guess you like

Origin blog.csdn.net/lrb0677/article/details/128275957