background

When you need to quickly implement an idea, if you use the Java stack, you usually choose the SpringBoot stack. Although SpringBoot solves the problems of traditional Spring and MVC configuration, and its ecosystem is very powerful, However, in practical use, basic components such as database, response encapsulation, exception interception, code generator and interface document still need to be integrated. Generally, there are two methods:

  1. The use of open source all kinds of background management systems, such systems are generally perfect modules, powerful functions; However, many irrelevant modules can cause some interference;
  2. Use a simplified version that integrates the most commonly used modules for ease of control and familiarity with how these mainstream technologies fit together while “re-building the wheel”.

Here, you build back-end scaffolding from scratch, assembling open source components in the form of building blocks. Subsequent toy projects were developed based on this scaffolding.

MybatisPlus code generator

Refer to the official code warehouse and document: mp.baomidou.com/guide/gener…

After simply modifying path information, run the command directlyMysqlGeneratorOf the classmainMethods. Type the module table and table name respectively to generateEntity , Mapper , Service , ControllerAnd other corresponding files.

MybatisPlus ID of the snowflake algorithm

The default primary key policy of MybatisPlus (3.3.1) is the Snowflake algorithm. If the primary key is not explicitly set, MybatisPlus code automatically calculates a value through the snowflake algorithm and inserts it as an ID when inserting.

SnowFlake is a Java Long integer of type Long, usually corresponding to MySQL BIGINT(20); It has the characteristic of monotonically increasing trend and globally unique.

MybatisPlus paging

  • MybatisPlusConfig.java
@Configuration
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor(a) {
        return new PaginationInterceptor().setCountSqlParser(new JsqlParserCountOptimize(true)); }}Copy the code
  • PageUtils.java
@Data
public class PageUtils {
    // Total number of records
    private long total;

    // Number of records per page
    private long size;

    / / the total number of pages
    private long pages;

    // The current number of pages
    private long current;

    // List data
    privateList<? > records;// Add it flexibly
    private Map<String,Object> data;

    /** ** paging *@paramRecords list data *@paramTotal Total records *@paramSize Number of records per page *@paramCurrent Number of pages */
    public PageUtils(List<? > records,long total, long size, long current) {
        this.records = records;
        this.total = total;
        this.size = size;
        this.current = current;
        this.pages = (long)Math.ceil((double)total/size);
    }

    /** ** paging *@paramRecords list data *@paramTotal Total records *@paramSize Number of records per page *@paramCurrent Number of pages */
    public PageUtils(List<? > records,long total, long size, long current, Map<String,Object> data) {
        this.records = records;
        this.total = total;
        this.size = size;
        this.current = current;
        this.data = data;
        this.pages = (long)Math.ceil((double)total/size);
    }

    /** ** paging */
    public PageUtils(Page
        page) {
        this.records = page.getRecords();
        this.total = (long)page.getTotal();
        this.size = page.getSize();
        this.current = page.getCurrent();
        this.pages = (long)page.getPages(); }}Copy the code
  • BookController.java
// Paging queries: use custom PageUtils
@GetMapping("list")
public Result<PageUtils> list(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
    PageUtils list = bookService.findList(new Page<>(page, size), params);
    return Result.success(list);
}

// Paging query: use the MyBatisPlus page method
@GetMapping("page")
public Result<IPage<Book>> page(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
    QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
    queryWrapper.likeRight("read_date", params.get("readDate"));
    IPage<Book> list = bookService.page(new Page<>(page, size),queryWrapper);
    return Result.success(list);
}
Copy the code

Unified response encapsulation

The RestControllerAdvice annotation intercepts the request and encapsulates the Result as Result.

@RestControllerAdvice
public class ResultAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;

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

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class
       > aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (o instanceof String) {
            return objectMapper.writeValueAsString(Result.success(o));
        }
        if (o instanceof Result) {
            return o;
        }
        returnResult.success(o); }}Copy the code
  • Result.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    /** Result status, normal response 200, other status codes are failed */
    private int code;
    private String msg;
    private T data;

    // Static methods
    /** * call */ on success
    public static <T> Result<T> success(T data) {
        return new Result<T>(data, CodeMsg.SUCCESS);
    }
    public static <T> Result<T> success(a) {
        return new Result<T>(CodeMsg.SUCCESS);
    }

    /** ** failed to call */
    public static <T> Result<T> error(Integer code, String msg) {
        return new Result<T>(code, msg);
    }
    public static <T> Result<T> error(CodeMsg codeMsg) {
        return new Result<T>(codeMsg);
    }
    public static <T> Result<T> error(String msg) {
        CodeMsg codeMsg = new CodeMsg(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg);
        return new Result<T>(codeMsg);
    }

    // Constructor
    private Result(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    private Result(T data, CodeMsg codeMsg) {
        this.data = data;
        if(codeMsg ! =null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg(); }}private Result(CodeMsg codeMsg) {
        if(codeMsg ! =null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg(); }}}Copy the code
  • CodeMsg.java
@Getter
public class CodeMsg {
    private int code;
    private String msg;

    // Common error code
    public static final CodeMsg SUCCESS =new CodeMsg(HttpStatus.OK.value(), "success");
    public static final CodeMsg BAD_REQUEST = new CodeMsg(HttpStatus.BAD_REQUEST.value(), "Request invalid");
    public static final CodeMsg SERVER_ERROR = new CodeMsg(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Server exception");
    public static final CodeMsg NO_HANDLER_FOUND = new CodeMsg(HttpStatus.NOT_FOUND.value(), "No corresponding resource found");
    public static final CodeMsg UNAUTHORIZED = new CodeMsg(HttpStatus.UNAUTHORIZED.value(), "Unauthenticated or login status expired");
    public static final CodeMsg FORBIDDEN = new CodeMsg(HttpStatus.FORBIDDEN.value(), "Not authorized");
    // Custom error code
    public static final CodeMsg PARAMETER_ERROR = new CodeMsg(4000."Incorrect parameters!");
    /* User related: verification code */
    public static final CodeMsg CAPTCHA_EXPIRED = new CodeMsg(4001."Verification code does not exist or has expired");
    public static final CodeMsg CAPTCHA_INVALID = new CodeMsg(4002."Verification code error");
    /* User related: authentication and authorization */
    public static final CodeMsg BAD_CREDENTIAL = new CodeMsg(4003."Wrong username or password");
    public static final CodeMsg ACCOUNT_NOT_FOUND = new CodeMsg(4004."Account does not exist");
    public static final CodeMsg ACCOUNT_NOT_ACTIVATED = new CodeMsg(4005."Account is not active");
    / / current limit
    public static final CodeMsg RATE_LIMIT = new CodeMsg(4006."Threshold reached!");
    / / fuse
    public static final CodeMsg DEGRADE = new CodeMsg(4007."It's fused!");

    public static CodeMsg error(String msg){
        return new CodeMsg(HttpStatus.BAD_REQUEST.value(),msg);
    }
    public CodeMsg(int code, String msg) {
        this.code = code;
        this.msg = msg; }}Copy the code

Global exception interception

Intercepts all exceptions by default (you can also encapsulate custom exceptions), and encapsulates exception responses using RestControllerAdvice annotations.

  • RestExceptionHandler.java
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<String> exception(Exception e) {
        log.error("Global exception: {}".null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(CodeMsg.SERVER_ERROR.getCode(), null== e.getMessage() ? e.toString() : e.getMessage()); }}Copy the code

The Controller of the CRUD

@RestController
@RequestMapping("book")
@API (tags = "Test Controller")
public class BookController {
    @Autowired
    IBookService bookService;

    @GetMapping("hello")
    @ ApiOperation (" hello ")
    public String hello(a) {
        return "hello everyone.";
    }

    @GetMapping("list")
    public List<Book> list(a) {
        return bookService.list();
    }

    @PostMapping("save")
    public boolean save(@RequestBody Book book) {
        return bookService.save(book);
    }

    @GetMapping("detail/{id}")
    public Result detail(@PathVariable long id) {
        return Result.success(bookService.getById(id));
    }

    @GetMapping("error")
    public Result error(a) {
        int value = 8 / 0;
        return Result.success(value);
    }

    @GetMapping("page")
    public Result<IPage<Book>> page(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
        QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
        queryWrapper.likeRight("read_date", params.get("readDate"));
        IPage<Book> list = bookService.page(new Page<>(page, size),queryWrapper);
        returnResult.success(list); }}Copy the code

Swagger3 Interface document

  • Introduction of depend on
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>${swagger.version}</version>
</dependency>
Copy the code
  • The configuration class
@Configuration
@EnableOpenApi
public class SwaggerConfig {
    private static final String VERSION = "1.0.0";
    @Bean
    public Docket createRestApi(a) {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.heartsuit.readingnotes.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo(a) {
        return new ApiInfoBuilder()
                .title("SpringBoot+Swgger3.0 Back-end service Interface documentation")
                .contact(new Contact("Heartsuit"."https://blog.csdn.net/u013810234"."[email protected]"))
                .description("Interface Documentation based on Swagger3.0)
                .termsOfServiceUrl("https://blog.csdn.net/u013810234")
                .license(The Apache License, Version 2.0)
                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html") .version(VERSION) .build(); }}Copy the code
  • Add annotations to the controller and interface
@API (tags = "Test Controller")
@RestController
public class HelloController {
    @GetMapping("hello")
    @ ApiOperation (" hello ")
    public String hello(a) {
        return "Hello SpringBoot with Swagger3.0"; }}Copy the code
  • Start service, browser access

Yes, there are no additional annotations, just start the service and access it in your browser.

Note:

  • Swagger2. X access address: http://localhost:8000/swagger-ui.html

  • Swagger3.0 access address: http://localhost:8000/swagger-ui/index.html

  • Switches that control the generation of documents

In practice our interface documentation will only be used in a development environment, so we will normally close the documentation in production.

  • application.yml
spring:
  profiles:
    active: dev
Copy the code
  • application-dev.yml
springfox:
  documentation:
    enabled: true
Copy the code
  • application-prod.yml
springfox:
  documentation:
    enabled: false
Copy the code

Problems encountered

  • Problem 1: Console printingMyBatisPlustheSQLThe log

Solutions:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
Copy the code
  • Question 2:LongType of snowflake algorithmIDAccuracy lost after transmission to the front end

Workaround: Always convert Long to string before returning back END JSON.

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        returnobjectMapper; }}Copy the code
  • Problem 3: Global exception handling matches the order of multiple exception handlers

Solutions:

Below, in addition to all the globally intercepted exceptions, there is a CustomException, CustomException. How do the current two exceptions match when a CustomException occurs? The answer is that the subclass exception handler takes precedence and is intercepted by the customException method, not the Exception method.

@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<String> exception(Exception e) {
        log.error("Global exception: {}".null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(CodeMsg.SERVER_ERROR.getCode(), null == e.getMessage() ? e.toString() : e.getMessage());
    }

    @ExceptionHandler(CustomException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<String> customException(CustomException e) {
        log.error("Custom exception: {}".null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(e.getCode(), null== e.getMessage() ? e.toString() : e.getMessage()); }}Copy the code
@Getter
public class CustomException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private Integer code;

    public CustomException(CodeMsg codeMsg) {
        super(codeMsg.getMsg());
        this.code = codeMsg.getCode();
    }
    public CustomException(Integer code, String msg){
        super(msg);
        this.code = code; }}Copy the code
  • Error 4: Access Swagger address Unable to infer base url. This is common when using dynamic servlet registration or when the API is behind an API Gateway. The base url is the root of where all the swagger resources are served. For e.g. if the api is available at Example.org/api/v2/api-… then the base url is example.org/api/. Please enter the location manually

Solutions:

The reason is that we use RestControllerAdvice to handle the interface response uniformly, so the return value for Swagger is also wrapped, and the browser cannot parse and render the page.

Change @RestControllerAdvice to @RestControllerAdvice(basePackages = “com.heartsuit.*.controller”)

That is, limit the interception scope of RestControllerAdvice to handle only interface responses under specified packets.

Project depend on

<properties>
        <java.version>11</java.version>
        <mybatisplus.version>3.3.1</mybatisplus.version>
        <swagger.version>3.0.0</swagger.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <! --Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <! --MySQL and ORM-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.21</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatisplus.version}</version>
        </dependency>

        <! - Swagger3.0 -- -- >
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>${swagger.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
Copy the code

The configuration file

server:
  port: 8000

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      url: jdbc:mysql://localhost:3306/reading_notes? serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useSSL=false
      username: root
      password: root

mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  typeAliasesPackage: com.heartsuit.*.entity

  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
Copy the code

IDEA related plug-ins

Plug-ins used:

  • LombokGenerate getters, setters, toString(), and log prints through annotations;
  • MyBatis Log PluginSQL+ parameter concatenation from MyBatis and MyBatisPlus console logs
  • RestfulToolkit: Test the interface of the control layer in IDEA without switching from IDE to browser or Postman;
  • Free Mybatis plugin: Link Mapper interface with XML

Reference

Blog.csdn.net/huishuaijun…


If you have any questions or any bugs are found, please feel free to contact me.

Your comments and suggestions are welcome!