文章目录
官方构建Spring Boot应用:http://start.spring.io/
对返回值 Json 的处理
Spring Boot 中默认使用的 json 解析框架是 jackson。
jackson 中对 null 的处理
在实际项目中,难免会遇到一些 null 值出现,在转 json 时,是不希望有这些 null 出现的,比如期望所有的 null 在转 json 时都变成 “” 这种空字符串。
在 Spring Boot 中,做一下配置即可,新建一个 jackson 的配置类:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.io.IOException;
@Configuration
public class JacksonConfig {
@Bean
@Primary
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("");
}
});
return objectMapper;
}
}
使用 alibaba fastJson 解析框架
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class AlibabaFastJsonConfig extends WebMvcConfigurationSupport {
/**
* 使用阿里 FastJson 作为JSON MessageConverter
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(
// 保留map空的字段
SerializerFeature.WriteMapNullValue,
// 将String类型的null转成""
SerializerFeature.WriteNullStringAsEmpty,
// 将Number类型的null转成0
SerializerFeature.WriteNullNumberAsZero,
// 将List类型的null转成[]
SerializerFeature.WriteNullListAsEmpty,
// 将Boolean类型的null转成false
SerializerFeature.WriteNullBooleanAsFalse,
// 避免循环引用
SerializerFeature.DisableCircularReferenceDetect);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(Charset.forName("UTF-8"));
List<MediaType> mediaTypeList = new ArrayList<>();
// 解决中文乱码问题,相当于在Controller上的@RequestMapping中加了个属性produces = "application/json"
mediaTypeList.add(MediaType.APPLICATION_JSON);
converter.setSupportedMediaTypes(mediaTypeList);
converters.add(converter);
}
}
日期格式处理
fastjson:@JSONField(format ="yyyy-MM-dd HH:mm:s")
jackson:@JsonFormat(pattern="yyyy-MM-dd HH:mm:s",timezone = "GMT+8")
封装统一返回的数据结构
@Data
public class JsonResult<T> {
private T data;
private Integer code;
private String msg;
/**
* 若没有数据返回,默认状态码为0,提示信息为:操作成功!
*/
public JsonResult() {
this.code = 0;
this.msg = "操作成功!";
}
/**
* 若没有数据返回,可以人为指定状态码和提示信息
*/
public JsonResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* 有数据返回时,状态码为0,默认提示信息为:操作成功!
*/
public JsonResult(T data) {
this.data = data;
this.code = 0;
this.msg = "操作成功!";
}
/**
* 有数据返回,状态码为0,人为指定提示信息
*/
public JsonResult(T data, String msg) {
this.data = data;
this.code = 0;
this.msg = msg;
}
}
全局异常处理
自定义业务异常:
@Data
public class BusinessErrorException extends RuntimeException {
private Integer code;
private String message;
public BusinessErrorException(Integer code, String message) {
this.code = code;
this.message = message;
}
}
全局异常处理类:
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 拦截业务异常,返回业务异常信息
*
* @param ex BusinessErrorException
*/
@ExceptionHandler(BusinessErrorException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleBusinessError(BusinessErrorException ex) {
Integer code = ex.getCode();
String message = ex.getMessage();
return new JsonResult(code, message);
}
/**
* 缺少请求参数异常
*
* @param ex MissingServletRequestParameterException
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public JsonResult handleHttpMessageNotReadableException(MissingServletRequestParameterException ex) {
log.error("缺少请求参数,{}", ex.getMessage());
return new JsonResult(HttpStatus.BAD_REQUEST.value(), "缺少必要的请求参数");
}
/**
* 空指针异常
*
* @param ex NullPointerException
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleTypeMismatchException(NullPointerException ex) {
log.error("空指针异常,{}", ex.getMessage(), ex);
return new JsonResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), "空指针异常了");
}
/**
* 系统异常 预期以外异常
*
* @param ex Exception
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleUnexpectedServer(Exception ex) {
log.error("系统异常:", ex);
return new JsonResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), "系统发生异常,请联系管理员");
}
}
使用 slf4j 进行日志记录
使用 slf4j 作为自己的日志框架。
spring:
profiles:
active: dev
logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextName>logback</contextName>
<!-- 日志存放路径 -->
<property name="log.path" value="logs"/>
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d [%t] %-5le %lo{50}[%L] >> %m%n"/>
<!-- 输出到控制台 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 输出到文件,level 为 ALL -->
<appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<encoder>
<pattern>${log.pattern}</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>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 只保留近90天的日志 -->
<maxHistory>90</maxHistory>
</rollingPolicy>
</appender>
<!-- 输出到文件,level 为 DEBUG 日志 -->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<encoder>
<pattern>${log.pattern}</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>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>90</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="REQUEST" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/request.log</file>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/request-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>90</maxHistory>
</rollingPolicy>
</appender>
<!-- 设置包下的日志输出到指定 appender,日志打印级别为 info -->
<logger name="com.example.boot.controller" level="info">
<appender-ref ref="REQUEST"/>
</logger>
<!-- 设置包下的日志打印级别为 debug -->
<logger name="com.example.boot.mapper" level="debug"/>
<!-- 指定开发环境日志输出级别为INFO,并且只绑定了CONSOLE的appender,表示将日志信息输出到控制台 -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- 指定生产环境日志输出级别为INFO,表示将日志信息输出到文件 -->
<springProfile name="prod">
<root level="info">
<appender-ref ref="FILE_ALL"/>
<appender-ref ref="FILE_ERROR"/>
</root>
</springProfile>
</configuration>
集成 Swagger2 接口文档
<!-- swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
<groupId>com.github.caspar-chen</groupId>
<artifactId>swagger-ui-layer</artifactId>
<version>1.1.3</version>
</dependency>
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
// 指定构建api文档的详细信息的方法:apiInfo()
.apiInfo(apiInfo())
.select()
// 指定要生成api接口的包路径,这里把controller作为包路径,生成controller中的所有接口
.apis(RequestHandlerSelectors.basePackage("com.example.boot.controller"))
.paths(PathSelectors.any())
.build();
}
/**
* 构建api文档的详细信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 设置页面标题
.title("XXX应用接口总览")
// 设置接口描述
.description("")
// 设置版本
.version("1.0")
// 构建
.build();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用户实体类")
public class User {
@ApiModelProperty(value = "用户唯一标识")
private Long id;
@ApiModelProperty(value = "用户姓名")
private String username;
@ApiModelProperty(value = "用户密码")
private String password;
}
import com.example.boot.config.JsonResult;
import com.example.boot.config.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Api(tags = "Swagger2 在线接口文档")
@Slf4j
public class HelloController {
@GetMapping("/get/{id}")
@ApiOperation(value = "根据用户唯一标识获取用户信息")
public JsonResult<User> getUserInfo(@PathVariable @ApiParam(value = "用户唯一标识") Long id) {
// 模拟数据库中根据id获取User信息
User user = new User(id, "James", "123456");
return new JsonResult(user);
}
@PostMapping("/insert")
@ApiOperation(value = "添加用户信息")
public JsonResult<Void> insertUser(@RequestBody @ApiParam(value = "用户信息") User user) {
// 处理添加逻辑
return new JsonResult<>();
}
}