前言
我们写接口的时候可能是写一个静态方法,包装返回值,这种做法有一些问题:
1、每个方法的返回都是Result封装对象,没有业务含义 2、在业务代码中,成功的时候我们调用Result.success,异常错误调用Result.failure。是不是很多余 3、上面的代码,判断id是否为null,其实我们可以使用validate做校验,没有必要在方法体中做判断。
实现思路
1、定义一个注解@ResponseResult,表示这个接口返回的值需要包装一下 2、拦截请求,判断此请求是否需要被@ResponseResult注解 3、核心步骤就是实现接口ResponseBodyAdvice和@ControllerAdvice,判断是否需要包装返回值,如果需要,就把Controller接口的返回值进行重写。
新建Maven项目
1、pom.xml文件
<groupId>com.fang</groupId>
<artifactId>result-response</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
</dependency>
</dependencies>
2、定义返回状态码,这里实现ResultCode
接口,为了可以扩展ResultCodeEnum
枚举类,自定义返回状态码。
public interface ResultCode {
Integer getCode();
String getMessage();
ResultCode setMessage(String message);
}
- setMessage方法用来自定义返回信息
public enum ResultCodeEnum implements ResultCode {
/**
* 成功状态码
*/
SUCCESS(0, "成功"),
/* 参数错误:1001-1999 */
PARAM_IS_INVALID(1001, "参数无效"),
PARAM_IS_BLANK(1002, "参数为空"),
PARAM_TYPE_ERROR(1003, "参数类型错误"),
PARAM_NOT_COMPLETE(1004, "参数缺失"),
/* 用户错误 */
USER_NOT_LOGIN(2001, "用户未登录"),
USER_LOGIN_ERROR(2002, "账号或密码错误"),
USER_ACCOUNT_FORBIDDEN(2003, "账号被禁用"),
USER_NOT_EXIST(2004, "用户不存在"),
USER_HAS_EXIST(2005, "用户已存在"),
/* 请求错误 */
METHOD_REQUEST_ERROR(3001, "请求方法错误"),
NOT_FOUND(3002, "请求地址不存在"),
/* 未知错误 */
UNKNOWN_ERROR(5000, "未知错误")
;
private Integer code;
private String message;
ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
@Override
public ResultCode setMessage(String message) {
this.message = message;
return this;
}
}
3、定义返回类
@Data
public class Result<T> implements Serializable {
private Integer code;
private String message;
private T data;
public Result(ResultCode resultCode, T data) {
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
this.data = data;
}
/**
* 返回成功
* @param <T>
* @return
*/
public static Result success() {
return new Result(ResultCodeEnum.SUCCESS, null);
}
/**
* 返回成功,带数据
* @param data
* @param <T>
* @return
*/
public static <T> Result success(T data) {
return new Result(ResultCodeEnum.SUCCESS, data);
}
/**
* 返回失败
* @param resultCode
* @return
*/
public static Result fail(ResultCode resultCode) {
return new Result(resultCode, null);
}
}
4、定义业务异常类,包装状态码接口
public class BusinessException extends RuntimeException {
private ResultCode resultCode;
/**
* Constructs a new runtime exception with {@code null} as its
* detail message. The cause is not initialized, and may subsequently be
* initialized by a call to {@link #initCause}.
*/
public BusinessException(ResultCode resultCode) {
this.resultCode = resultCode;
}
public ResultCode getResultCode() {
return resultCode;
}
}
5、定义全局异常处理类
@Slf4j
@RestControllerAdvice
public class ExceptionController {
@ExceptionHandler(BusinessException.class)
public ResultCode handleException(BusinessException ex) {
log.info(ex.getMessage());
return ex.getResultCode();
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultCode parameterExceptionHandler(MethodArgumentNotValidException e) {
log.info(e.getMessage());
// 获取异常信息
BindingResult exceptions = e.getBindingResult();
// 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
if (exceptions.hasErrors()) {
List<ObjectError> errors = exceptions.getAllErrors();
if (!errors.isEmpty()) {
// 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
FieldError fieldError = (FieldError) errors.get(0);
return ResultCodeEnum.PARAM_IS_INVALID.setMessage(fieldError.getDefaultMessage());
}
}
return ResultCodeEnum.PARAM_IS_INVALID;
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result methodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
log.info(ex.getMessage());
return Result.fail(ResultCodeEnum.METHOD_REQUEST_ERROR);
}
@ExceptionHandler(NoHandlerFoundException.class)
public Result noHandlerFoundException(NoHandlerFoundException ex) {
log.info(ex.getMessage());
return Result.fail(ResultCodeEnum.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResultCode otherException(Exception ex) {
log.info(ex.getMessage());
return ResultCodeEnum.UNKNOWN_ERROR.setMessage(ex.getMessage());
}
}
- 注意:
HttpRequestMethodNotSupportedException
和NoHandlerFoundException
是直接返回结果,其它接口是返回状态码,因为它们没有到Controller层,@ControllerAdvice
拦截不了,所以直接返回。 parameterExceptionHandler
是处理@Valid
校验异常
6、自定义注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseResult {
}
7、定义拦截器,判断是否有ResponseResult
注解,是否需要返回值包装,设置一个属性标记。
@Slf4j
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
public static final String RESPONSE_RESULT = "RESPONSE_RESULT";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("request = " + request.getServerName());
log.info(this.toString());
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Class<?> beanType = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
if (beanType.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(RESPONSE_RESULT, beanType.getAnnotation(ResponseResult.class));
} else if (method.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(RESPONSE_RESULT, method.getAnnotation(ResponseResult.class));
}
log.info("Attribute: " + request.getAttribute(RESPONSE_RESULT));
}
return true;
}
}
8、返回接口重写处理
@Slf4j
@RestControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
ResponseResult attribute = (ResponseResult) request.getAttribute(ResponseResultInterceptor.RESPONSE_RESULT);
if (attribute != null) {
return true;
}
return false;
}
@Nullable
@Override
public Object beforeBodyWrite(@Nullable Object t, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
log.info("进入返回体重写处理中: " + t.toString());
if (t instanceof ResultCode) {
ResultCode resultCode = (ResultCode) t;
return Result.fail(resultCode);
}
if (t instanceof Result) {
return t;
}
return Result.success(t);
}
}
9、打包成starter项目
spring.factories文件:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.fang.config.ResponseConfig
定义Java配置类:
@Configuration
@ComponentScan
public class ResponseConfig {
@Bean
public ResponseResultHandler responseResultHandler() {
return new ResponseResultHandler();
}
@Bean
public ExceptionController exceptionController() {
return new ExceptionController();
}
}
- 注意:要能注入到Spring IOC容器里需要加上
@ComponentScan
注解
注入拦截器:
@Configuration
public class MyConfigurerAdapter extends WebMvcConfigurationSupport {
@Bean
public ResponseResultInterceptor responseResultInterceptor() {
return new ResponseResultInterceptor();
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(responseResultInterceptor());
}
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}
configureMessageConverters
是为了解决该项目引入到其它项目时出现的类型转换问题com.fang.response.Result cannot be cast to java.lang.String
,原因是因为在所有的HttpMessageConverter
实例集合中,StringHttpMessageConverter
要比其它的Converter
排得靠前一些。我们需要将处理Object
类型的HttpMessageConverter
放得靠前一些,所以当返回String
类型是就会报错。其它类型没问题。
最后使用mvn install
打包。
使用说明
1、创建Spring Boot项目,引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fang</groupId>
<artifactId>result-response</artifactId>
<version>1.0</version>
</dependency>
2、在启动类添加注解,使拦截器生效。
@SpringBootApplication
@Import({MyConfigurerAdapter.class})
public class ReponseUsedemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(ReponseUsedemoApplication.class, args);
}
}
3、其它的跟以前没有区别,只是在Controller加上@ResponseResult
即可,该返回什么类型就返回什么类型。@ResponseResult
注解也可以加在方法上。
@Api
@ResponseResult
@RestController
@RequestMapping("order")
public class OrderController {
@ApiOperation("订单详情")
@PostMapping("detail")
public List<OrderVo> getOrder(@RequestBody @Valid OrderVo orderVo) {
return new ArrayList<OrderVo>(){};
}
@RequestMapping("test")
public OrderVo getTest() {
return new OrderVo();
}
@RequestMapping("str")
public String getTest2() {
return "123";
}
@GetMapping("name")
public String getDetail(@RequestParam String name) {
if (name == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
return "hello";
}
}
需要自定义返回码时,可以实现ResultCode
接口:
public enum ErrorCode implements ResultCode {
SYSTEM_ERROR(9999, "系统错误")
;
private Integer code;
private String message;
ErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getMessage() {
return this.message;
}
@Override
public ResultCode setMessage(String s) {
this.message = s;
return this;
}
}
存在问题
使用swagger时,访问swagger-ui.html
返回404,提供的解决方案是:
@Configuration
public class SwaggerMvnConfiguration extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0);
}
}
这样能解决问题,但是会覆盖引入依赖的配置,使得拦截器失效。网上说用WebMvcConfigurer
,当时没有作用,试了很久发现需要加上@EnableWebMvc
注解才有作用。
@Configuration
@EnableWebMvc
public class SwaggerConfiguration implements WebMvcConfigurer {
/**
* 通用拦截器排除swagger设置,所有拦截器都会自动加swagger相关的资源排除信息
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0);
}
}
当同样会覆盖掉引入依赖的配置,使得拦截器失效。
解决
一个项目中只能有一个继承WebMvcConfigurationSupport的@Configuration类(使用@EnableMvc效果相同),如果存在多个这样的类,只有一个配置可以生效。推荐使用 implements WebMvcConfigurer 的方法自定义mvc配置。
@Configuration
@EnableWebMvc
public class MyConfigurerAdapter implements WebMvcConfigurer {
@Bean
public ResponseResultInterceptor responseResultInterceptor() {
return new ResponseResultInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(responseResultInterceptor());
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}
才有这种方式启动类可以不需要使用@Import({MyConfigurerAdapter.class})