1. Introduction

Today we start building our Kono Spring Boot scaffolding. We will integrate Spring MVC and customize it to meet our daily development needs. We will do some rigid requirements customization first and fill in the details later. If you read this and have any questions, please leave a comment. More continuous attention, common learning, common progress.

Gitee: gitee.com/felord/kono

Making: github.com/NotFound403…

2. Unified return body

It is important to return data uniformly in development. Convenient front-end unified processing. Usually designed for the following structure:

{
    "code": 200."data": {
        "name": "felord.cn"."age": 18
    },
    "msg": ""."identifier": ""
}
Copy the code
  • Code Indicates the service status code, which must be different from the HTTP status code.
  • Data data carrier, used to load the data returned to the front-end display.
  • MSG Prompt message, which is returned after the front-end invocation, for example, Adding succeeded or deleting failed.
  • Identifier Reserved identifier used for processing certain services.

Based on some of the above definitions, a unified return body object, RestBody

, is declared and static methods are declared for easy definition.

package cn.felord.kono.advice;

import lombok.Data;

import java.io.Serializable;

/ * * *@author felord.cn
 * @since22:32 2019-04-02 * /
@Data
public class RestBody<T> implements Rest<T>, Serializable {

    private static final long serialVersionUID = -7616216747521482608L;
    private int code = 200;
    private T data;
    private String msg = "";
    private String identifier = "";
 

    public staticRest<? > ok() {return new RestBody<>();
    }

    public staticRest<? > ok(String msg) { Rest<? > restBody =new RestBody<>();
        restBody.setMsg(msg);
        return restBody;
    }

    public static <T> Rest<T> okData(T data) {
        Rest<T> restBody = new RestBody<>();
        restBody.setData(data);
        return restBody;
    }

    public static <T> Rest<T> okData(T data, String msg) {
        Rest<T> restBody = new RestBody<>();
        restBody.setData(data);
        restBody.setMsg(msg);
        return restBody;
    }


    public static <T> Rest<T> build(int code, T data, String msg, String identifier) {
        Rest<T> restBody = new RestBody<>();
        restBody.setCode(code);
        restBody.setData(data);
        restBody.setMsg(msg);
        restBody.setIdentifier(identifier);
        return restBody;
    }

    public staticRest<? > failure(String msg, String identifier) { Rest<? > restBody =new RestBody<>();
        restBody.setMsg(msg);
        restBody.setIdentifier(identifier);
        return restBody;
    }

    public staticRest<? > failure(inthttpStatus, String msg ) { Rest<? > restBody =new RestBody< >();
        restBody.setCode(httpStatus);
        restBody.setMsg(msg);
        restBody.setIdentifier("9999");
        return restBody;
    }

    public static <T> Rest<T> failureData(T data, String msg, String identifier) {
        Rest<T> restBody = new RestBody<>();
        restBody.setIdentifier(identifier);
        restBody.setData(data);
        restBody.setMsg(msg);
        return restBody;
    }

    @Override
    public String toString(a) {
        return "{" +
                "code:" + code +
                ", data:" + data +
                ", msg:" + msg +
                ", identifier:" + identifier +
                '} '; }}Copy the code

However, it’s not very elegant to explicitly declare the return body every time, so we want to implement this function imperceptibly. The Spring Framework provides just this functionality, with the help of @RestControllerAdvice and ResponseBodyAdvice

to do post-cut notification processing on the response body of the control class for each @RestController tag of the project.

/** * uniformly returns the body wrapper **@author felord.cn
 * @since14:58 * * /
@RestControllerAdvice
public class RestBodyAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter methodParameter, Class
       > aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class
       > aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        // If null returns a null body with no data
        if (o == null) {
            return RestBody.ok();
        }
        Return if the parent class of RestBody is the parent type of the return value
        // So we can return the RestBody directly in the interface method
        if (Rest.class.isAssignableFrom(o.getClass())) {
            return o;
        }
        // Perform unified return body encapsulation
        returnRestBody.okData(o); }}Copy the code

When our interface returns an entity class, it is automatically encapsulated in the unified return body RestBody

.

Since there is ResponseBodyAdvice, there is a RequestBodyAdvice, which seems to be there for pre-processing and may have some use later.

2. Unified exception handling

Uniform exceptions are also implemented by @RestControllerAdvice (see Hibernate Validator). Validation exception handling is initially integrated here, and additional exceptions will be added later.

/** * unified exception handling **@author felord.cn
 * @since 13 :31  2019-04-11
 */
@Slf4j
@RestControllerAdvice
public class ApiExceptionHandleAdvice {

    @ExceptionHandler(BindException.class)
    publicRest<? > handle(HttpServletRequest request, BindException e) { logger(request, e); List<ObjectError> allErrors = e.getAllErrors(); ObjectError objectError = allErrors.get(0);
        return RestBody.failure(700, objectError.getDefaultMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    publicRest<? > handle(HttpServletRequest request, MethodArgumentNotValidException e) { logger(request, e); List<ObjectError> allErrors = e.getBindingResult().getAllErrors(); ObjectError objectError = allErrors.get(0);
        return RestBody.failure(700, objectError.getDefaultMessage());
    }

    @ExceptionHandler(ConstraintViolationException.class)
    publicRest<? > handle(HttpServletRequest request, ConstraintViolationException e) { logger(request, e); Optional<ConstraintViolation<? >> first = e.getConstraintViolations().stream().findFirst(); String message = first.isPresent() ? first.get().getMessage() :"";
        return RestBody.failure(700, message);
    }


    @ExceptionHandler(Exception.class)
    publicRest<? > handle(HttpServletRequest request, Exception e) { logger(request, e);return RestBody.failure(700, e.getMessage());
    }


    private void logger(HttpServletRequest request, Exception e) {
        String contentType = request.getHeader("Content-Type");
        log.error("Unified exception handling URI: {} Content-Type: {} Exception: {}", request.getRequestURI(), contentType, e.toString()); }}Copy the code

3. Simplified type conversions

Simplifying transformations between Java beans is also a necessary feature. I’m going to choose mapStruct, which is type-safe and easy to use, much better than BeanUtil. But in my experience with mapStruct, don’t just do simple mappings with the complexity that mapStruct provides. For details, see Spring Boot 2: Integrating MapStruct conversion.

The scope of the reference is compile only at compile time, and we add the dependencies to kono-dependencies.

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${mapstruct.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${mapstruct.version}</version>
    <scope>compile</scope>
</dependency>
Copy the code

Reference the above two dependencies directly in Kono-app, but this is not enough; compiling with Lombok is prone to SPI errors. We also need to integrate the relevant Maven plug-ins into the kono-app build lifecycle. For reference:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <showWarnings>true</showWarnings>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </path>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>${mapstruct.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
Copy the code

We can then easily convert one Java Bean into another. The following code converts UserInfo to UserInfoVO and automatically assigns UserInfoVO. AddTime to the current time, and the tool also automatically injects Spring IoC, all at compile time.

Compile the front:

/ * * *@author felord.cn
 * @since16:09 * * /
@Mapper(componentModel = "spring", imports = {LocalDateTime.class})
public interface BeanMapping {

    @Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
    UserInfoVO toUserInfoVo(UserInfo userInfo);

}
Copy the code

The compiled:

package cn.felord.kono.beanmapping;

import cn.felord.kono.entity.UserInfo;
import cn.felord.kono.entity.UserInfoVO;
import java.time.LocalDateTime;
import javax.annotation.Generated;
import org.springframework.stereotype.Component;

@Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2020-07-30T23:11:24+0800", comments = "version: Final, Compiler: JavAC, Environment: Java 1.8.0_252 (AdoptOpenJDK)")
@Component
public class BeanMappingImpl implements BeanMapping {

    @Override
    public UserInfoVO toUserInfoVo(UserInfo userInfo) {
        if ( userInfo == null ) {
            return null;
        }

        UserInfoVO userInfoVO = new UserInfoVO();

        userInfoVO.setName( userInfo.getName() );
        userInfoVO.setAge( userInfo.getAge() );

        userInfoVO.setAddTime( LocalDateTime.now() );

        returnuserInfoVO; }}Copy the code

MapStruct actually writes getters and setters for us, but don’t use its more complicated transformations, which can be costly to learn and difficult to maintain.

Unit testing

After integrating the above functions, do a unit test and pass them all.

    @Autowired
    MockMvc mockMvc;
    @Autowired
    BeanMapping beanMapping;

    /** * Tests global exception handling. **@throws Exception the exception
     * @see UserController#getUserInfo()
     */
    @Test
    void testGlobalExceptionHandler(a) throws Exception {

        String rtnJsonStr = "{\n" +
                " \"code\": 700,\n" +
                " \"data\": null,\n" +
                " \"msg\": \"test global exception handler\",\n" +
                " \"identifier\": \"-9999\"\n" +
                "}";

        mockMvc.perform(MockMvcRequestBuilders.get("/user/get"))
                .andExpect(MockMvcResultMatchers.content()
                        .json(rtnJsonStr))
                .andDo(MockMvcResultHandlers.print());
    }

    /** * test unified return body. **@throws Exception the exception
     * @see UserController#getUserVO()
     */
    @Test
    void testUnifiedReturnStruct(a) throws Exception {
// "{\" code \ ": 200, \" data \ ": {\" name \ ": \" felord. Cn \ ", \ "age \" : 18, \ "addTime \" : \ "of" the 2020-07-30 T13:08:53. 201 \}, \ "MSG \" : \ "\", \ "the ident ifier\":\"\"}";
        mockMvc.perform(MockMvcRequestBuilders.get("/user/vo"))
                .andExpect(MockMvcResultMatchers.jsonPath("code", Is.is(200)))
                .andExpect(MockMvcResultMatchers.jsonPath("data.name", Is.is("felord.cn")))
                .andExpect(MockMvcResultMatchers.jsonPath("data.age", Is.is(18)))
                .andExpect(MockMvcResultMatchers.jsonPath("data.addTime", Is.is(notNullValue())))
                .andDo(MockMvcResultHandlers.print());
    }


    /** * Test mapStruct type conversion. **@see BeanMapping
     */
    @Test
    void testMapStruct(a) {
        UserInfo userInfo = new UserInfo();
        userInfo.setName("felord.cn");
        userInfo.setAge(18);
        UserInfoVO userInfoVO = beanMapping.toUserInfoVo(userInfo);

        Assertions.assertEquals(userInfoVO.getName(), userInfo.getName());
        Assertions.assertNotNull(userInfoVO.getAddTime());
    }
Copy the code

5. To summarize

Homemade scaffolding has a unified return body, unified exception processing, fast type conversion, in fact, parameter verification has been supported. The subsequent integration of the database, the commonly used database access technology is mainly Mybatis, Spring Data JPA, JOOQ and so on, DO not know which one you prefer? Welcome to comment.

Follow our public id: Felordcn for more information

Personal blog: https://felord.cn