统一返回Maven包实现

Posted by Kaka Blog on November 5, 2020

前言

我们写接口的时候可能是写一个静态方法,包装返回值,这种做法有一些问题:

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());
    }
}
  • 注意:HttpRequestMethodNotSupportedExceptionNoHandlerFoundException是直接返回结果,其它接口是返回状态码,因为它们没有到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})

参考资料