一、sweet-boot-web-starter是什么?
sweet-boot-web-starter 是基于spring-boot和swagger组件二次封装的组件,是sweet-boot中重要的一个启动装置。方便快速搭建web开发工程,快速生成接口的API文档,API统一响应格式和API统一异常处理。
二、封装
1.创建sweet-boot-web-starter模块,编辑pom文件:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lyd.sweet</groupId>
<artifactId>sweet-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>sweet-boot-web-starter</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.lyd.sweet</groupId>
<artifactId>sweet-boot-common</artifactId>
</dependency>
<!--org.springframework.boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>
<!--swagger接口-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<!--validation-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!--lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2.创建swagger配置类
/**
* swagger2配置文件,集成swagger,通过配置可设置swagger是否开启,标题,扫描路径等
* 配置示例
* sweet:
* api-doc:
* enabled: true
* title: api文档
* description: 文档描述
* basePackage: com.biz.xxx
*
* @author 木木
*/
@EnableSwagger2
@ConditionalOnProperty(prefix = "sweet.api-doc", name = "enabled", havingValue = "true")
@ConfigurationProperties(prefix = "sweet.api-doc")
public class SwaggerConfig implements WebMvcConfigurer {
private static Logger logger = LoggerFactory.getLogger(SwaggerConfig.class);
/**
* 是否启用api 文档,如swagger
*/
private boolean enabled = false;
/**
* api文档标题
*/
private String title = "sweet-web-api";
/**
* api文档描述
*/
private String description = "api文档描述";
/**
* 自动扫描文档注解的根路径
*/
private String basePackage = "com.lyd";
/**
* api版本
*/
private String version = "1.0.0";
@Bean
public Docket createRestApi() {
logger.info("sweet swagger ApiDoc Init {}", enabled);
return new Docket(DocumentationType.SWAGGER_2)
//正式服务器设置为false
.enable(enabled)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (enabled) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title + " Restful API")
.description(description)
.contact(new Contact("木木", "http://localhost", ""))
.version(version)
.build();
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getBasePackage() {
return basePackage;
}
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
}
3.创建API统一响应格式处理程序类
/**
* 针对RestController的统一返回值包装。
* 引用微服务在controller中都直接返回原始结果,由该类统一包装成ResultObject对象返回。
* 确保所有api返回格式一致。
*
* @author 木木
**/
@RestControllerAdvice(basePackages = "com")
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
private static Logger logger = LoggerFactory.getLogger(ResponseResultHandler.class);
/**
* Handler配置
*/
private final SweetHandlerProperties handlerProperties;
public ResponseResultHandler(SweetHandlerProperties handlerProperties) {
this.handlerProperties = handlerProperties;
}
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
//如果RestController的返回值已经是ResponseObject则不处理。
return !methodParameter.getGenericParameterType().equals(ResponseObject.class);
}
/**
* 重写结构体输出写处理
*
* @param o
* @param methodParameter
* @param mediaType
* @param aClass
* @param serverHttpRequest
* @param serverHttpResponse
* @return
*/
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
Object reObj = this.bodyWrite(o, methodParameter);
if (methodParameter.getGenericParameterType().equals(String.class)) {
try {
ObjectMapper objectMapper = new ObjectMapper();
//返回值为String时Content-Type为text/html。这里强制转为application/json, 编码为UTF-8
if (reObj instanceof String) {
return o;
} else {
serverHttpResponse.getHeaders().set("Content-Type", "application/json;charset=UTF-8");
return objectMapper.writeValueAsString(ResponseObject.success(o));
}
} catch (Throwable e) {
logger.warn("响应结果处理程序返回结果失败。返回值为:{},error-msg:{}" , o.toString(), e.getMessage());
return o;
}
} else if (o instanceof InputStreamResource) {
return o;
}
//非String对象的返回值,直接包装成ResultObject返回,如果是JSON,强制加上UTF-8编码
if (mediaType.toString().equals(MediaType.APPLICATION_JSON_VALUE)) {
serverHttpResponse.getHeaders().set("Content-Type", "application/json;charset=UTF-8");
}
return reObj;
}
/**
* 输出具体的对象信息
*
* @param o
* @param methodParameter
* @return
*/
public Object bodyWrite(Object o, MethodParameter methodParameter) {
if (!this.isFormatRequest(methodParameter)) {
return o;
}
if (o instanceof ResponseObject) {
return o;
}
return ResponseObject.success(o);
}
/**
* 判断是否包装
*
* @param methodParameter
* @return true 要包装HEAD信息 false不需要包装
*/
public boolean isFormatRequest(MethodParameter methodParameter) {
boolean re = handlerProperties.isResultHandlerEnabled();
// 处理类上注解
SweetResponseHandler classAnnotation = methodParameter.getContainingClass().getAnnotation(SweetResponseHandler.class);
SweetResponseHandler methodAnnotation = methodParameter.getMethodAnnotation(SweetResponseHandler.class);
// 如果类上有注解,先按类处理
if (!ObjectUtils.isEmpty(classAnnotation)) {
re = classAnnotation.sFormat();
}
// 如果方法上有注解,按方法上处理
if (!ObjectUtils.isEmpty(methodAnnotation)) {
re = methodAnnotation.sFormat();
}
return re;
}
}
4.创建API统一异常处理程序类
/**
* Rest服务统一、通用的异常处理
* 针对引用此的rest微服务抛出的所有异常统一在此处理,统一在此记录日志。
* 使用@RestControllerAdvice注解,针对RestController ,Response Content-Type值统一为application/json;charset=UTF-8
* http请求的状态码统一返回:400. HttpStatus.BAD_REQUEST
* 原则上RestController代码只负责记录好异常信息抛出合适的异常,不需要考虑异常日志的记录。
* 对于异常发生后业务代码要进行补救处理的(如:重试,回滚等特殊处理),这类异常应该在业务代码中处理,不在本类中处理。
* <p>
* 具体异常处理设计原则:
* 1、所有异常都会在本类中捕获。
* 2、如果RestController中的方法已经被执行到了,所有产生的异常我们认为是中台的业务异常,统一封装成ResponseObject抛出并记录日志。
* 3、RestController中的方法被执行之前产生的异常,统一交给Spring框架或web服务中间件去进行原生的处理。
* 比如404 405等各类HTTP异常。如果个别有需要处理,单独捕获进行处理。
*
* @author 木木
**/
@RestControllerAdvice
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 异常时日志是否显示详细请求内容
*/
private final Boolean showRequestDetailWhenErr;
public GlobalExceptionHandler(SweetHandlerProperties handlerProperties) {
this.showRequestDetailWhenErr = handlerProperties.isResultHandlerEnabled();
}
/**
* ServletException通用处理
* 所有http请求的各类异常都继承于ServletException。这里捕获后统一不做处理,直接抛出,交给Spring和web容器去处理。
*
* @param e
* @return
* @throws ServletException
*/
@ExceptionHandler(ServletException.class)
public ResponseObject ServletExceptionHandle(ServletException e) throws ServletException {
throw e;
}
/**
* Exception 异常统一处理
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public ResponseObject exceptionHandler(Exception e) {
logger.error("异常处理程序-未知异常!{},{}", e.getMessage(), getReqMessage(), e);
return ResponseObject.fail(ServiceStatus.FAIL);
}
/**
* RuntimeException 运行时异常统一处理
*
* @param e
* @return
*/
@ExceptionHandler(RuntimeException.class)
public ResponseObject runtimeExceptionHandler(RuntimeException e) {
logger.error("异常处理程序-未知运行时异常!{},{}", e.getMessage(), getReqMessage(), e);
return ResponseObject.fail(ServiceStatus.FAIL);
}
/**
* 服务参数异常的统一处理
*
* @param e
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseObject methodArgumentNotValidExceptionHandle(MethodArgumentNotValidException e) {
String paramsFailMsg = this.getParamsFailMsg(e.getBindingResult());
logger.error("异常处理程序-服务方法参数校验异常!{},{}", paramsFailMsg, getReqMessage(), e);
return ResponseObject.fail(String.format("服务方法参数校验异常%s", paramsFailMsg));
}
/**
* 服务参数异常的统一处理(表单方式处理)
*
* @param result
* @return
*/
@ExceptionHandler(BindException.class)
public ResponseObject methodArgumentNotValidExceptionHandle(BindingResult result) {
String paramsFailMsg = this.getParamsFailMsg(result);
logger.error("异常处理程序-服务方法参数校验异常!{},{}", paramsFailMsg, getReqMessage());
return ResponseObject.fail(String.format("服务方法参数校验异常%s", paramsFailMsg));
}
/**
* 请求参数缺失
*
* @param e
* @return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseObject methodArgumentNotValidExceptionHandle(MissingServletRequestParameterException e) {
logger.error("异常处理程序-缺少请求参数!{}", getReqMessage(), e);
return ResponseObject.fail("缺少请求参数");
}
/**
* 类型转换异常
*
* @param ex
* @return
*/
@ExceptionHandler(ClassCastException.class)
public ResponseObject classCastExceptionHandler(ClassCastException ex) {
logger.error("异常处理程序-类型转换异常!{}", getReqMessage(), ex);
return ResponseObject.fail("类型转换异常");
}
/**
* IO异常
*
* @param ex
* @return
*/
@ExceptionHandler(IOException.class)
public ResponseObject iOExceptionHandler(IOException ex) {
logger.error("异常处理程序-IO异常!{}", getReqMessage(), ex);
return ResponseObject.fail("IO异常");
}
/**
* 数组越界异常
*
* @param ex
* @return
*/
@ExceptionHandler(IndexOutOfBoundsException.class)
public ResponseObject indexOutOfBoundsExceptionHandler(IndexOutOfBoundsException ex) {
logger.error("异常处理程序-数组越界异常!{}", getReqMessage(), ex);
return ResponseObject.fail("数组越界异常");
}
/**
* 方法参数类型不匹配异常
*
* @param ex
* @return
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseObject methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
logger.error("异常处理程序-方法参数类型不匹配异常!{}", getReqMessage());
return ResponseObject.fail("方法参数类型不匹配异常");
}
/**
* 自定义根异常的默认处理
*
* @param e
* @return
*/
@ExceptionHandler(SweetBasicException.class)
public ResponseObject sweetBaseExceptionHandler(SweetBasicException e) {
logger.error("异常处理程序-自定义异常!{}", getReqMessage(), e);
//对于自定义异常,如果有传入message则使用,没有则使用ServiceStatus对应的描述
String message = (e.getMessage() != null && !e.getMessage().equals("")) ? e.getMessage() : e.getServiceStatus().getDesc();
return ResponseObject.fail(e.getServiceStatus(), message);
}
/**
* sql异常的通用处理
*
* @param e
* @return
*/
@ExceptionHandler(SQLException.class)
public ResponseObject sqlExceptionHandle(SQLException e) {
logger.error("异常处理程序-SQL处理异常!{}", getReqMessage(), e);
return ResponseObject.fail("SQL处理异常");
}
/**
* 解析错误的通用处理
*
* @param e
* @return
*/
@ExceptionHandler(ParseException.class)
public ResponseObject parseExceptionHandle(ParseException e) {
logger.error("异常处理程序-方法解析异常!{}", getReqMessage(), e);
return ResponseObject.fail("方法解析异常");
}
/**
* 空指针异常
*
* @param ex
* @return
*/
@ExceptionHandler(NullPointerException.class)
public ResponseObject nullPointerExceptionHandler(NullPointerException e) {
logger.error("异常处理程序-空指针异常!{}", e.getMessage(), e);
return ResponseObject.fail("空指针异常");
}
/**
* 找不到方法异常
*
* @param ex
* @return
*/
@ExceptionHandler(NoSuchMethodException.class)
public ResponseObject noSuchMethodExceptionHandler(NoSuchMethodException ex) {
logger.error("异常处理程序-找不到方法异常!{}", getReqMessage(), ex);
return ResponseObject.fail("找不到方法异常");
}
/**
* 获取api请求详细
*
* @return
*/
private String getReqMessage() {
if (showRequestDetailWhenErr) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ObjectUtils.isEmpty(servletRequestAttributes)) {
return "";
}
HttpServletRequest request = servletRequestAttributes.getRequest();
String uri = request.getRequestURI();
String showFormat = "service run err, uri is : [%s], params is : [%s]";
Map<String, String[]> params = request.getParameterMap();
Map<String, String> showRes = new HashMap<>(16);
params.forEach((key, value) -> {
String valueStr = value == null || value.length == 0 ?
"" : String.join(",", value);
showRes.put(key, valueStr);
});
String paramsStr = showRes.toString();
return String.format(showFormat, uri, paramsStr);
}
return "";
}
/**
* 获取参数异常信息
*
* @param bindingResult
* @return
*/
private String getParamsFailMsg(BindingResult bindingResult) {
if (!ObjectUtils.isEmpty(bindingResult)
&& !bindingResult.getAllErrors().isEmpty()) {
List<String> list = new ArrayList<>(8);
for (ObjectError error : bindingResult.getAllErrors()) {
String msg = error.getDefaultMessage();
list.add(msg);
}
return CollectionUtils.isEmpty(list) ? "" : Arrays.toString(list.toArray());
}
return "";
}
}
5.创建/resource/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lyd.sweet.config.SwaggerConfig,\
com.lyd.sweet.config.WebMvcEndpointConfig,\
com.lyd.sweet.properties.SweetHandlerProperties,\
com.lyd.sweet.handler.GlobalExceptionHandler,\
com.lyd.sweet.handler.ResponseResultHandler
三、使用步骤
1.引入库
<dependency>
<groupId>com.lyd.sweet</groupId>
<artifactId>sweet-boot-web-starter</artifactId>
</dependency>
2.示例
sweet-boot-web-starter: 使用示例 - Gitee.com