掘金 后端 ( ) • 2024-04-15 09:28

开发环境

JDK 21
spring-boot 3.2.3

IDEA配置:

  1. 推荐开启自动导入功能 File -> Editor -> general -> Auto import
  2. maven helper插件可以进行依赖分析, 解决jar包冲突

新建

IDEA新建一个项目 选择spring initializr,勾选spring web依赖和lombok,或者手动添加

pom.xml添加依赖

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

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
</dependency>

把application.properties改为application.yml
由于默认启动端口为8080,容易被占用,自定义一个端口

spring:
  profiles:
    active: dev
    
server:
  port: 9999

新建一个测试controller

package com.example.backendtemplate.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("user")
@RestController
public class userController {

    @GetMapping("/login")
    public String login() {
        return "login success";
    }
}

点击访问http://127.0.0.1:9999/user/login 得到返回结果login success

统一返回值

包装正常返回

GlobalResultDto.java


package com.example.backendtemplate.config;

import lombok.Data;
import org.springframework.http.HttpStatus;

@Data
public class GlobalResultDto<T> {

    // http状态码
    private int code;

    // 返回消息
    private String msg;

    // 返回数据
    private T data;

    public GlobalResultDto() {
        this.code = HttpStatus.OK.value();
        this.msg = "success";
    }

    public GlobalResultDto(T data) {
        this.code = HttpStatus.OK.value();
        this.msg = "success";
        this.data = data;
    }

    public static GlobalResultDto error(String message) {
        GlobalResultDto responseEntity = new GlobalResultDto();
        responseEntity.setMsg(message);

        responseEntity.setCode(HttpStatus.BAD_REQUEST.value());
        return responseEntity;
    }
    
    public static GlobalResultDto success() {
        GlobalResultDto responseEntity = new GlobalResultDto();
        responseEntity.setCode(HttpStatus.OK.value());
        return responseEntity;
    }

    public void setCode(int retCode) {
        this.code = retCode;
    }
}

HttpStatus可以使用自定义错误码

GlobalReturnConfig.java

@RestControllerAdvice
public class GlobalReturnConfig implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 过滤掉已经包装过返回值的接口和spring docs
        return !(returnType.getParameterType().equals(ResponseEntity.class)
            || returnType.getDeclaringClass().getName().contains("springdoc"));
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
        Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
        ServerHttpResponse response) {

        if (body instanceof GlobalResultDto) {
            return body;
        }

        return new GlobalResultDto<>(body);
    }
}

请求,查看日志会发现报错

错误处理 cannot be cast to class java.lang.String

错误的原因是HttpMessageConvertor调用顺序有问题,调整一下

GlobalReturnWebConfig.java

@Configuration
@Slf4j
public class GlobalReturnWebConfig implements WebMvcConfigurer {
    /**
     * 交换MappingJackson2HttpMessageConverter和第二位元素
     * 让返回值类型为String的接口能正常返回包装结果
     *
     * @param converters initially an empty list of converters
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (int i = 0; i < converters.size(); i++) {
            if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
                MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter
                    = (MappingJackson2HttpMessageConverter) converters.get(i);
                converters.set(i, converters.get(1));
                converters.set(1, mappingJackson2HttpMessageConverter);
                break;
            }
        }
    }
}

不要用浏览器访问,浏览器默认无法解析我们返回的对象,使用postmam或者下面的swagger可以看到返回数据正常了

{
	"code": 200,
	"msg": "success",
	"data": "login success"
}

包装错误返回

修改一下方法


@SneakyThrows
public String login() {
    throw new Exception("出错了");
}

查看接口报错返回和我们上面的返回不一样,是因为报错的情况需要单独处理

GlobalExceptionHandler.java

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 处理参数校验异常
     *
     * @param req the request
     * @param ex the exception
     * @return result body
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public GlobalResultDto handleValidException(HttpServletRequest req, MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getAllErrors()
            .stream()
            .map(err -> err.getDefaultMessage())
            .collect(Collectors.toList());
        return GlobalResultDto.error(String.join(",", errors));
    }

    /**
     * 处理未知的异常
     *
     * @param req the request
     * @param ex the exception
     * @return result body
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public GlobalResultDto handleUnknownException(HttpServletRequest req, Exception ex) {
        String msg = ex.getMessage() == null ? ex.getCause().getMessage() : ex.getMessage();
        return GlobalResultDto.error(msg);
    }
}

集成接口文档springdoc

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>

重新启动然后访问:
http://127.0.0.1:9999/swagger-ui/index.html

问题:无法正常显示

只显示了示例接口,没有我们的接口,这是因为出错了,把链接改为/v3/api-docs,F12查看发现接口有报错,网上一查发现是之前我们改的两个地方影响了

  1. 全局返回封装影响到了,过滤掉就行
  2. converters中第一个就是ByteArrayMessageConverter,这个对是spring doc是有用的,我们可以把MappingJackson2HttpMessageConverter放在第二位

上面的代码都是修改后的的,不需修改,只是记录一下这个问题

集成Knife4j

文档地址:https://doc.xiaominfo.com/docs/quick-start

默认的界面风格不好用

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.4.0</version>
</dependency>

访问http://127.0.0.1:9999/doc.html

参考文章:
https://blog.csdn.net/OriginalSword/article/details/135201484 https://blog.csdn.net/nyzzht123/article/details/129855860

日志

我们发现调用接口默认没有接口相关日志

添加配置

logging:
  level:
    root: info
    org.springframework.web.servlet.DispatcherServlet: debug
    org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor: debug

定制输出文件

主要功能是: 按级别记录 + 按大小拆分 + 定时清理 参考文章: https://blog.csdn.net/xu_san_duo/article/details/80364600

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,比如: 如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration  scan="true" scanPeriod="10 seconds">
    <contextName>logback</contextName>

    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
    <springProperty scope="context" name="myLogLocation" source="logging.file-location" default="/var/log/myapp"/>
    <property name="log.path" value="${myLogLocation}"/>

    <!--0. 日志格式和颜色渲染 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!--1. 输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--2. 输出到文档-->
    <!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/debug.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档 -->
            <fileNamePattern>${log.path}/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录debug级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>debug</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 2.2 level为 INFO 日志,时间滚动输出  -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/info.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 2.3 level为 WARN 日志,时间滚动输出  -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/warn.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 2.4 level为 ERROR 日志,时间滚动输出  -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/error.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    
    <!-- 2.5 所有 除了DEBUG级别的其它高于DEBUG的 日志,记录到一个文件  -->
    <appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/all.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/all-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档记录除了DEBUG级别的其它高于DEBUG的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
    </appender>

    <springProfile name="dev">
        <root level="info">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="ALL_FILE" />
        </root>
        <logger name="com.xusanduo.demo" level="debug"/> <!-- 开发环境, 指定某包日志为debug级 -->
    </springProfile>

    <springProfile name="test">
        <root level="info">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="ALL_FILE" />
        </root>
        <logger name="com.xusanduo.demo" level="info"/> <!-- 测试环境, 指定某包日志为info级 -->
    </springProfile>

    <springProfile name="pro">
        <root level="info">
            <!-- 生产环境最好不配置console写文件 -->
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="ALL_FILE" />
        </root>
        <logger name="com.xusanduo.demo" level="warn"/> <!-- 生产环境, 指定某包日志为warn级 -->
        <logger name="com.xusanduo.demo.MyApplication" level="info"/> <!-- 特定某个类打印info日志, 比如application启动成功后的提示语 -->
    </springProfile>

</configuration>

自定义日志

参考文章:https://blog.csdn.net/CSDN2497242041/article/details/122323507

微服务下的日志链路跟踪

java agent + mdc

健康检查

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

默认只开启/health,可以配置开放其它接口,为了安全,不要开启不必要的接口

management:
  endpoints:
    web:
      exposure:
        include: info, health

访问http://127.0.0.1:9999/actuator/health

数据库配置

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.5.4</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

spring.sql.init

resources/sql/schema.sql

CREATE TABLE IF NOT EXISTS test.t_user_group
(
    id          SERIAL PRIMARY KEY,
    group_name  VARCHAR(255) NOT NULL,
    group_owner VARCHAR(255) NOT NULL,
    create_time TIMESTAMP    NOT NULL,
    update_time TIMESTAMP    NOT NULL
    );

CREATE TABLE IF NOT EXISTS test.t_user
(
    id          SERIAL PRIMARY KEY,
    username    VARCHAR(255) NOT NULL,
    "password"  VARCHAR(255) NOT NULL,
    create_time TIMESTAMP    NOT NULL,
    update_time TIMESTAMP    NOT NULL
);

账号密码下面都是默认值

spring:
  profiles:
    default: dev
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/postgres?currentSchema=test
    username: postgres
    password:
  sql:
    init:
      mode: always
      schema-locations: classpath:sql/schema.sql
      platform: postgres
      continue-on-error: false

打印执行日志:

把这个类设置成debug就可以显示执行sql的日志

org.springframework.jdbc.datasource.init.ScriptUtils: debug

liqiubase

使用spring.sql.init已经能满足我们普通项目的大部分需求了,但在团队开发中,我们还要考虑版本记录,归档,迁移等功能,还需要用到类似liqiubase的工具

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>4.25.0</version>
</dependency>
# spring.sql.init配置需要删除,不然会重复执行
#  sql:
#    init:
#      mode: always
#      schema-locations: classpath:sql/schema.sql
#      platform: postgres
#      continue-on-error: false
  liquibase:
    enabled: true
    change-log: classpath:/liquibase/master.yaml

resources/liquibase/master.yaml配置文件

databaseChangeLog:
  - includeAll:
      path: classpath*:sql/schema.sql
      errorIfMissingOrEmpty: false

重新执行会发现日志会打印执行的信息,以及数据库多了两张表,是保存的执行记录

mybatis plus 自动生成CRUD

配置

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.5</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.5</version>
</dependency>
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
</dependency>

打印具体的sql日志配置

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

下面是自定义模板需要用到的类型方法

@Data
public class BaseSelectParams {
    /**
     * 是否升序
     */
    private Boolean isAsc = true;
    /**
     * 排序字段
     */
    private List<String> sortByList = new ArrayList<>();
    /**
     * 当前页数
     */
    private Integer pageNumber = 0;
    /**
     * 每页数据量
     */
    private Integer pageSize = 10;
}
@Data
public class SearchParams<T> {
    BaseSelectParams baseParams;
    T searchDto;
}

SFunctionUtil.java

public class SFunctionUtil {

    public static SFunction getSFunction(Class<?> entityClass, String fieldName) {

        Field field = getDeclaredField(entityClass, fieldName, true);
        field.setAccessible(true);
        if(field == null){
            throw ExceptionUtils.mpe("This class %s is not have field %s ", entityClass.getName(), fieldName);
        }
        SFunction func = null;
        final MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(field.getType(), entityClass);
        final CallSite site;
        String getFunName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        try {
            site = LambdaMetafactory.altMetafactory(lookup,
                "invoke",
                MethodType.methodType(SFunction.class),
                methodType,
                lookup.findVirtual(entityClass, getFunName, MethodType.methodType(field.getType())),
                methodType, FLAG_SERIALIZABLE);
            func = (SFunction) site.getTarget().invokeExact();

            return func;
        } catch (Throwable e) {
            throw ExceptionUtils.mpe("This class %s is not have method %s ", entityClass.getName(), getFunName);
        }
    }
}

config/mybatis/MyBatisUtils.java

public class MyBatisUtils {
    public static Map<String, Object> objectToMap(Object obj) throws Exception {
        Map<String, Object> map = new HashMap<>();

        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            String fieldName = field.getName();

            if (StringUtils.equals(fieldName, "serialVersionUID")) {
                continue;
            }
            Object fieldValue = field.get(obj);
            log.info(fieldName, fieldValue);
            if (ObjectUtils.isNotEmpty(fieldValue)) {
                map.put(fieldName, fieldValue);
            }
        }
        return map;
    }

    @SneakyThrows
    public static <T, M extends BaseMapper<T>> Page<T> commonSearch(M mapper, SearchParams<T> searchParams) {

        Page<T> page = new Page<>();
        LambdaQueryWrapper<T> queryWrapperLamba = new QueryWrapper<T>().lambda();

        if (ObjectUtils.isNotEmpty(searchParams.getBaseParams())) {
            page.setCurrent(searchParams.getBaseParams().getPageNumber());
            page.setSize(searchParams.getBaseParams().getPageSize());

            List<SFunction<T, ?>> sortList = new ArrayList<>();
            for (String entry : searchParams.getBaseParams().getSortByList()) {
                sortList.add(getSFunction(searchParams.getSearchDto().getClass(), entry));
            }
            queryWrapperLamba.orderBy(true, searchParams.getBaseParams().getIsAsc(), sortList);
        }

        if (ObjectUtils.isNotEmpty(searchParams.getSearchDto())) {
            Map<String, Object> map = objectToMap(searchParams.getSearchDto());

            Map<SFunction<T, ?>, Object> transformedMap = new HashMap<>();

            // 遍历原始 Map,并转换 key
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                transformedMap.put(getSFunction(searchParams.getSearchDto().getClass(), entry.getKey()), entry.getValue());
            }
            queryWrapperLamba.allEq(transformedMap);
        }

        return mapper.selectPage(page, queryWrapperLamba);
    }
}

报错 Invalid value type for attribute ‘factoryBeanObjectType‘: java.lang.String

参考:https://github.com/baomidou/mybatis-plus/issues/5962
错误原因: mybatis plus不兼容新的spring-boot版本,3.1.8可以,后面的版本都会报这个错误 解决方案:使用最新版本的mybatis-spring

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis-spring</artifactId>
                <version>3.0.3</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
报错 JSON parse error: Cannot deserialize value of type java.time.LocalDateTime from String

有两种方式,推荐全局配置

  1. 在LocalDateTime上变量添加转换规则 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
  2. 配置全局转换
    config/LocalDateTimeSerializerConfig.java
@Configuration
public class LocalDateTimeSerializerConfig {
    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    private static final String DATE_PATTERN = "yyyy-MM-dd";

    /**
     * string转localdate
     */
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
        return new Converter<String, LocalDate>() {
            @Override
            public LocalDate convert(String source) {
                if (source.trim().length() == 0) {
                    return null;
                }
                try {
                    return LocalDate.parse(source);
                } catch (Exception e) {
                    return LocalDate.parse(source, DateTimeFormatter.ofPattern(DATE_PATTERN));
                }
            }
        };
    }

    /**
     * string转localdatetime
     */
    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        return new Converter<String, LocalDateTime>() {
            @Override
            public LocalDateTime convert(String source) {
                if (source.trim().length() == 0) {
                    return null;
                }
                // 先尝试ISO格式: 2019-07-15T16:00:00
                try {
                    return LocalDateTime.parse(source);
                } catch (Exception e) {
                    return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DATE_TIME_PATTERN));
                }
            }
        };
    }

    /**
     * 统一配置
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        JavaTimeModule module = new JavaTimeModule();
        LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
        return builder -> {
            builder.simpleDateFormat(DATE_TIME_PATTERN);
            builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN)));
            builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)));
            builder.modules(module);
        };
    }
}

配置Mapper目录

@MapperScan( {"com.example.xxxx.mapper"})
public class BackendTemplateApplication {

    public static void main(String[] args) {
        SpringApplication.run(BackendTemplateApplication.class, args);
    }

}

自动生成模板代码

MyBatis-Plus文档https://baomidou.com/pages/779a6e/#%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8
下面是我自定义的模板,父包名记得修改, 有时候生成的文件没显示需要手动reload from disk

config/mybatis/CodeGenerator.java

public class CodeGenerator {

    public static void main(String[] args) {

        List<String> tables = new ArrayList<>();
        tables.add("t_user_group");
        tables.add("t_user");

        FastAutoGenerator.create("jdbc:postgresql://localhost:5432/postgres?currentSchema=test", "postgres", "")
            .globalConfig(builder -> {
                builder.author("admin").disableOpenDir()
                    // .enableSwagger()
                    .commentDate("yyyy-MM-dd hh:mm:ss").outputDir(System.getProperty("user.dir") + "/src/main/java");
            })
            .packageConfig(builder -> {
                // builder.parent("com.example.backendtemplate")
                //     .service("domain.repository")
                //     .serviceImpl("infrastructure.repository.pgsql")
                //     .controller("facade.rest")
                //     .xml("infrastructure.dao.pgsql")
                //     .entity("infrastructure.dao.entity")
                //     .mapper("infrastructure.dao.pgsql")
                //     .pathInfo(Collections.singletonMap(OutputFile.xml,
                //         System.getProperty("user.dir") + "/src/main/resources/mapper"));
                builder.parent("com.example.backendtemplate") // 设置父包名
                    .service("server")
                    .serviceImpl("server.impl")
                    .controller("controller")
                    .xml("mapper")
                    .entity("entity")
                    .mapper("mapper")
                        .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper"));;
            })
            .strategyConfig(builder -> {
                builder.addInclude(tables) // 设置需要生成的表名

                    // Entity 策略配置
                    .entityBuilder()
                    .enableFileOverride()
                    .enableLombok()
                    .naming(NamingStrategy.underline_to_camel)
                    .columnNaming(NamingStrategy.underline_to_camel)

                    // Mapper 策略配置
                    .mapperBuilder()
                    .enableFileOverride()
                    .enableBaseResultMap()
                    .enableBaseColumnList()
                    .formatMapperFileName("%sMapper")

                    // Service 策略配置
                    .serviceBuilder()
                    .enableFileOverride()
                    .formatServiceFileName("%sService")
                    .formatServiceImplFileName("%sServiceImpl")

                    // Controller 策略配置
                    .controllerBuilder()
                    .enableFileOverride();

            })
            .templateConfig(new Consumer<TemplateConfig.Builder>() {
                @Override
                public void accept(TemplateConfig.Builder builder) {
                    // 实体类使用我们自定义模板
                    builder.controller("templates/myController.java");
                    builder.entity("templates/myEntity.java");
                }
            })
            .templateEngine(new FreemarkerTemplateEngine())

            .execute();
    }
}

resources/templates/myController.java.ftl

package ${package.Controller};

import static ${package.Parent}.config.mybatis.MyBatisUtils.commonSearch;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
<#if superControllerClassPackage??>
    import ${superControllerClassPackage};
</#if>
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import ${package.Service}.${table.entityName}Service;
import ${package.Entity}.${table.entityName};

/**
 *
 * ${table.comment!} 前端控制器
 *
 * @author ${author}
 * @since ${date}
 */

@Tag(name = " ${table.controllerName}", description = "${table.entityPath}控制器")
@Slf4j
@Validated
@RestController
@AllArgsConstructor
@RequestMapping("<#if package.ModuleName?? && package.ModuleName != "">/${package.ModuleName}</#if>/<#if controllerMappingHyphenStyle>${controllerMappingHyphen}<#else>${table.entityPath}</#if>")

<#if superControllerClass??>
public class ${table.controllerName} extends ${superControllerClass} {
<#else>
public class ${table.controllerName} {

    private final ${table.serviceName} ${table.entityPath}Service;

    private final ${table.mapperName} ${table.entityPath}Mapper;

    @Operation(summary = "查询列表")
    @PostMapping("search")
    public Page<${table.entityName}> search(@RequestBody SearchParams<${table.entityName}> searchParams) {
        return commonSearch(${table.entityPath}Mapper, searchParams);
    }

    @Operation(summary = "获取详情")
    @GetMapping(value = "/{id}")
    public ${table.entityName} findOne(@PathVariable @NotNull Integer id) {
         log.info("findOne: " + id);
         return ${table.entityPath}Service.getById(id);
    }

    @Operation(summary = "创建")
    @PostMapping()
    public Integer create(@RequestBody @Valid ${table.entityName} info) {
        log.info("create: ", info);
        Integer id = null;
        boolean flag =  ${table.entityPath}Service.save(info);
        if (flag) {
            id = info.getId();
        }
        return id;
    }

    @Operation(summary = "删除")
    @DeleteMapping(value = "/{id}")
    public boolean remove(@PathVariable @NotNull Integer id) {
        return ${table.entityPath}Service.removeById(id);
    }

    @Operation(summary = "更新")
    @PutMapping(value = "/{id}")
    public boolean update(@PathVariable @NotNull Integer id, @RequestBody @Valid ${table.entityName} info) {
        System.out.println("update: " + id + info);
        info.setId(id);
        return ${table.entityPath}Service.saveOrUpdate(info);
    }
</#if>
}

resources/templates/myEntity.java.ftl

package ${package.Entity};

<#list table.importPackages as pkg>
import ${pkg};
</#list>
<#if springdoc>
import io.swagger.v3.oas.annotations.media.Schema;
<#elseif swagger>
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
</#if>
<#if entityLombokModel>
import lombok.Data;
    <#if chainModel>
import lombok.experimental.Accessors;
    </#if>
</#if>

/**
 * <p>
 * ${table.comment!}
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
<#if entityLombokModel>
@Data
    <#if chainModel>
@Accessors(chain = true)
    </#if>
</#if>
<#if table.convert>
@TableName("${schemaName}${table.name}")
</#if>
<#if springdoc>
@Schema(name = "${entity}", description = "$!{table.comment}")
<#elseif swagger>
@ApiModel(value = "${entity}对象", description = "${table.comment!}")
</#if>
<#if superEntityClass??>
public class ${entity} extends ${superEntityClass}<#if activeRecord><${entity}></#if> {
<#elseif activeRecord>
public class ${entity} extends Model<${entity}> {
<#elseif entitySerialVersionUID>
public class ${entity} implements Serializable {
<#else>
public class ${entity} {
</#if>
<#if entitySerialVersionUID>

    private static final long serialVersionUID = 1L;
</#if>
<#-- ----------  BEGIN 字段循环遍历  ---------->
<#list table.fields as field>
    <#if field.keyFlag>
        <#assign keyPropertyName="${field.propertyName}"/>
    </#if>

    <#if field.comment!?length gt 0>
        <#if springdoc>
    @Schema(description = "${field.comment}")
        <#elseif swagger>
    @ApiModelProperty("${field.comment}")
        <#else>
    /**
     * ${field.comment}
     */
        </#if>
    </#if>

    <#if field.keyFlag>
        <#-- 主键 -->
        <#if field.keyIdentityFlag>
    @TableId(value = "${field.annotationColumnName}", type = IdType.AUTO)
        <#elseif idType??>
    @TableId(value = "${field.annotationColumnName}", type = IdType.${idType})
        <#elseif field.convert>
    @TableId("${field.annotationColumnName}")
        </#if>
        <#-- 普通字段 -->
    <#elseif field.fill??>
    <#-- -----   存在字段填充设置   ----->
        <#if field.convert>
    @TableField(value = "${field.annotationColumnName}", fill = FieldFill.${field.fill})
        <#else>
    @TableField(fill = FieldFill.${field.fill})
        </#if>
    <#elseif field.convert>
    @TableField("${field.annotationColumnName}")
    </#if>
    <#-- 乐观锁注解 -->
    <#if field.versionField>
    @Version
    </#if>
    <#-- 逻辑删除注解 -->
    <#if field.logicDeleteField>
    @TableLogic
    </#if>
    private ${field.propertyType} ${field.propertyName};
</#list>
<#------------  END 字段循环遍历  ---------->
<#if !entityLombokModel>
    <#list table.fields as field>
        <#if field.propertyType == "boolean">
            <#assign getprefix="is"/>
        <#else>
            <#assign getprefix="get"/>
        </#if>

    public ${field.propertyType} ${getprefix}${field.capitalName}() {
        return ${field.propertyName};
    }

    <#if chainModel>
    public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
    <#else>
    public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
    </#if>
        this.${field.propertyName} = ${field.propertyName};
        <#if chainModel>
        return this;
        </#if>
    }
    </#list>
</#if>
<#if entityColumnConstant>
    <#list table.fields as field>

    public static final String ${field.name?upper_case} = "${field.name}";
    </#list>
</#if>
<#if activeRecord>

    @Override
    public Serializable pkVal() {
    <#if keyPropertyName??>
        return this.${keyPropertyName};
    <#else>
        return null;
    </#if>
    }
</#if>
<#if !entityLombokModel>

    @Override
    public String toString() {
        return "${entity}{" +
    <#list table.fields as field>
        <#if field_index==0>
            "${field.propertyName} = " + ${field.propertyName} +
        <#else>
            ", ${field.propertyName} = " + ${field.propertyName} +
        </#if>
    </#list>
        "}";
    }
</#if>
}

添加分页插件

config/mybatis/MybatisPlusConfig.java

@Configuration
@MapperScan("com.example.backendtemplate.mapper")
public class MybatisPlusConfig {

    /**
     * 添加分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();

        // 设置数据库类型
        paginationInnerInterceptor.setDbType(DbType.POSTGRE_SQL);

        // 是否对超过最大分页时做溢出处理
        paginationInnerInterceptor.setOverflow(true);

        // 添加分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor);

        return interceptor;
    }
}

插入或更新数据时自动填充属性

config/mybatis/MyMetaObjectHandler.java

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

使用时在字段上添加TableField注解就可以了

@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

生成的mapper文件会发现一个警告expected, got 'id'

这是由于Base_Column_List不是一个完整的sql, idea识别有问题,改一下配置

image.png

问题记录 搜索参数处理

上面查询部分拼接参数比较复杂,主要是因为使用普通的QueryWrapper不会帮你转换名称,获取私有变量必须设置forceAccess为true, 查询接口包括的功能: 分页 + 排序 + 任意字段匹配筛选,可以满足大部分普通场景的需求

参考文章: https://blog.csdn.net/shuaizai88/article/details/117980854

获取配置信息

1. @values

只能一个一个添加,参数少的情况可以使用

import org.springframework.beans.factory.annotation.Value;

@Value("${server.port}") String serverPort;

2. @ConfigurationProperties

添加多个时比较方便

@Data
@ConfigurationProperties(prefix = "server")
public class ServerProperties {
    private String port;
}

@SpringBootApplication
@ConfigurationPropertiesScan
public class BackendTemplateApplication {
    public static void main(String[] args) {
        SpringApplication.run(BackendTemplateApplication.class, args);
    }
}
错误 Spring Boot Configuration Annotation Processor not configured

要添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
错误 Not registered via @EnableConfigurationProperties, marked as Spring component, or scanned via @ConfigurationPropertiesScan

没有注册成功,需要添加@Component注解或者在Application添加@ConfigurationPropertiesScan

多次读取body

封装一个多次读取的body

InputStream只能读取一次,我们可以把body缓存下来每次读取就返回一个新的InputStream

package com.example.backendtemplate.config;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
    private final String body;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();

        try (InputStream inputStream = request.getInputStream();
            BufferedReader bufferedReader = inputStream == null
                ? null
                : new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
            if (bufferedReader != null) {
                char[] charBuffer = new char[128];
                int bytesRead;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            }
        } catch (IOException ex) {
            log.error(ex.getMessage());
        }
        body = stringBuilder.toString();
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
            body.getBytes(StandardCharsets.UTF_8));
        return new MyServletInputStream(byteArrayInputStream);
    }

    public String getBody() {
        return this.body;
    }

    private static class MyServletInputStream extends ServletInputStream {
        private final ByteArrayInputStream byteArrayInputStream;

        public MyServletInputStream(ByteArrayInputStream byteArrayInputStream) {
            this.byteArrayInputStream = byteArrayInputStream;
        }

        // 重新封装ServletInputStream,检查输入流是否已完成,解决输入流只用一次问题
        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
        }

        @Override
        public int read() {
            return byteArrayInputStream.read();
        }
    }
}

inputstream无法多次读取,实际是可以实现mark和reset方法,注册参考下方的Filter部分

Filter

声明Filter

RequestFilter.java

public class RequestFilter implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    /**
     * 接口过滤方法
     *
     * @param servletRequest servlet请求头
     * @param servletResponse servlet回合头
     * @param filterChain 过滤链
     * @throws IOException IO异常
     * @throws ServletException servlet异常
     */
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
        if (!(servletRequest instanceof HttpServletRequest)) {
            return;
        }
        RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
        filterChain.doFilter(requestWrapper, servletResponse);

    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

注册Filter

FilterConfig.java

@Configuration
public class FilterConfig {
    @Bean
    public RequestFilter requestFilter() {
        return new RequestFilter();
    }

    @Bean
    public FilterRegistrationBean<RequestFilter> requestFilterRegistration(RequestFilter requestFilter) {
        FilterRegistrationBean<RequestFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(requestFilter);
        registration.addUrlPatterns("/*");
        registration.setOrder(1);
        return registration;
    }
}

推荐文章:
https://blog.csdn.net/z69183787/article/details/127808802 注册filter的两种方式

远程http请求调用

HttpClient难用、OKhttp、RestTemplate差不多, 一般选择Okhttp

  • spring boot2.x可以使用openFeign + Okhttp, Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池。 可以用OKhttp替换,从而获取连接池、超时时间等与性能息息相关的控制能力。
  • spring boot3.x可以使用内置的声明式HTTP 客户端,和openFeign类似
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

参考文章:https://www.jhelp.net/p/xAIEU94FrDNB6AIp

常见问题

是否需要统一返回结果

都可以,由于遇到的项目大部分是封装的,默认也就用封装的,谁负责搭建项目,谁决定

配置文件选择properties 还是 yml

差不多,但由于项目中用的都是yml,所以也默认使用yml了
讨论:https://www.v2ex.com/t/900871

Log4J2还是LogBack

差不多,网上的文章说是Log4J2的异步写入性能更强 https://cloud.tencent.com/developer/article/1704305

MVC还是DDD

谨慎使用DDD模式 代码质量依赖开发人员本身,而不是依赖架构,不管mvc还是ddd, 都可以写出好代码, 一般的项目MVC足够了,复杂且重要的项目可以用ddd, 而且一定要提前培训,检视代码,保证代码质量

是否需要微服务

绝大部分项目没有必要,类似中台,或业务量很复杂的情况才考虑拆分成微服务, 微服务不单是是项目本身的拆分,还需要一整套支持设施

微服务技术链路:

  • CI/CD 服务
  • k8s容器部署平台
  • 负责均衡
  • 配置中心
  • 数据库容灾
  • 全链路跟踪服务与日志服务
  • ELK 进行日志采集以及统一处理
  • apm 监测应用性能
  • aiops 智能运维

cacheable支持缓存时间

https://zhuanlan.zhihu.com/p/560218399

ObjectMapper的坑

ObjectMapper objectMapper = new ObjectMapper();
		
// 序列化的时候序列对象的所有属性
objectMapper.setSerializationInclusion(Include.ALWAYS);

// 反序列化的时候如果多了其他属性,不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// 如果是空对象的时候,不抛异常
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

// 取消时间的转化格式,默认是时间戳,可以取消,同时需要设置要表现的时间格式
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))