深入浅出Spring Boot接口

如何优雅的写 Controller 层代码?
瞧瞧,人家这后端API接口写得,那叫一个巴适~,再看看我的,像坨屎!

统一返回格式

定义一个业务CODE枚举类

public enum ResultCodeEnum {

    SUCCESS(1, "成功"),
    //参数错误

    ILLEGAL_PARAMETER(10001, "非法参数");

    private Integer code;
    private String msg;

    ResultCodeEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }
    
    public String getMsg() {
        return msg;
    }
}

定义统一返回类

public class R<T> implements Serializable {

    private T data;
    private Integer code;
    private String msg;

    public static <T> R<T> ok(T data) {
        R<T> r = new R<>();
        r.setData(data);
        return r;
    }

    public static <T> R<T> ok(ResultCodeEnum codeEnum, T data) {
        R<T> r = new R<>();
        r.setData(data);
        r.setCode(codeEnum.getCode());
        r.setMsg(codeEnum.getMsg());
        return r;
    }

    public static <T> R<T> fail(Integer code, String msg) {
        R<T> r = new R<>();
        r.setCode(code);
        r.setMsg(msg);
        return r;
    }

    public static <T> R<T> fail(ResultCodeEnum codeEnum) {
        R<T> r = new R<>();
        r.setCode(codeEnum.getCode());
        r.setMsg(codeEnum.getMsg());
        return r;
    }
	//省略部分代码
}

这样每个接口都需要使用R.ok()包裹返回值,下面我们通过一些配置来实现自动包裹。

定义一个封装注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface Result {
}

ResponseBodyAdvice接口

Allows customizing the response after the execution of an @ResponseBody or a ResponseEntity controller method but before the body is written with an HttpMessageConverter.

这个接口作用在Controller返回结果之后和written with an HttpMessageConverter之前。

true if beforeBodyWrite should be invoked; false otherwise

重写supports方法 ,返回true调用beforeBodyWrite 。

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        //方法所在class
        Class<?> methodClass = returnType.getContainingClass();
        //获取注解
        Result r = methodClass.getAnnotation(Result.class);
        //如果不存在不处理
        if (ObjectUtils.isEmpty(r)) {
            return false;
        }
        //如果已经是R返回false,如果不是R返回true
        boolean assignableFrom = returnType.getParameterType().isAssignableFrom(R.class);
        return !assignableFrom;
    }

当接口返回String类型报错如下

R cannot be cast to java.lang.String
	at org.springframework.http.converter.StringHttpMessageConverter.addDefaultHeaders

Invoked after an HttpMessageConverter is selected and just before its write method is invoked.

beforeBodyWrite 调用在HttpMessageConverter选择后和write 方法调用之前。

重写beforeBodyWrite方法

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

        if (body instanceof String) {
            try {
                ObjectMapper objectMapper = new ObjectMapper();
                //修改返回类型
                response.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
                R<Object> r = R.ok(body);
                return objectMapper.writeValueAsString(r);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return R.ok(body);
    }

到这里我们就不需要使用R.ok来包裹返回类型了。

selectedContentType和selectedConverterType是选定的ContentType和ConverterType

打印一下这两个值

        logger.info("selectedContentType:" + selectedContentType);
        logger.info("selectedConverterType:" + selectedConverterType);

输出

//String
selectedConverterType:class org.springframework.http.converter.StringHttpMessageConverter
selectedContentType:text/html
//其他
selectedContentType:application/json
selectedConverterType:class org.springframework.http.converter.json.MappingJackson2HttpMessageConverter

那么谁调用的 beforeBodyWrite 这个方法呢 ?

beforeBodyWrite 调用过程

在方法内部打一个断点定位到RequestResponseBodyAdviceChain,processBody中141行调用beforeBodyWrite

				body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
						contentType, converterType, request, response);

beforeBodyWrite中116行调用processBody

		return processBody(body, returnType, contentType, converterType, request, response);

AbstractMessageConverterMethodProcessor中268行调用了beforeBodyWrite

					body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
							(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
							inputMessage, outputMessage);

全局异常处理

迭代到这里,只需要在类上加入@Result注解就可以实现接口的封装处理,那么异常的情况呢?

在ResultBodyAdvice中加入这一段代码,通过code和msg来统一处理异常。

    @ExceptionHandler(Exception.class)
    public R runtimeExceptionHandle(Throwable e) {
        if (RuntimeException.class.equals(e.getClass())) {
            return R.fail(ResultCodeEnum.SERVER_ERROR);
        } else if (IllegalArgumentException.class.equals(e.getClass())) {
            return R.fail(ResultCodeEnum.ILLEGAL_PARAMETER);
        } else {
            return R.fail(3000, "其他错误");
        }
    }

参数校验

SpringBoot 还在用 if 校验参数?那你真的太low了,老司机都是这么玩的!

目的:消灭 if 条件判断。

约定:Get请求不传body,Post请求通过body传参。
RequestParam绑定url参数,RequestBody 绑定body体中参数。

@RequestParam("id") Integer id
@RequestBody String name

@Validated

添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
public class Hello6VO {
    @Min(value = 10, message = "最小值10")
    private Integer id;

    @Min(value = 5, message = "最小值5")
    private Integer id2;
}
    @GetMapping("hello6")
    public void hello6(@Validated Hello6VO vo) {
        System.out.println(vo.getId());
    }

    @PostMapping("hello7")
    public void hello7(@RequestBody @Validated  Hello6VO vo) {
        System.out.println(vo.getId());
    }

捕获失败异常

else if (e instanceof org.springframework.validation.BindException) {
            BindException exception = (org.springframework.validation.BindException) e;
            //优先处理字段错误
            List<FieldError> fieldErrorsList = exception.getBindingResult().getFieldErrors();
            List<String> fieldErrorMessageList = new ArrayList<>(fieldErrorsList.size());
            //获取默认提示信息列表
            for (FieldError fieldError : fieldErrorsList) {
                //字段名
                String field = fieldError.getField();
                //提示信息
                String message = fieldError.getDefaultMessage();
                //拒绝的值
                Object rejectedValue = fieldError.getRejectedValue();
                String str = "字段:" + field + " 提示信息:" + message + " 拒绝的值:" + rejectedValue;
                fieldErrorMessageList.add(str);
            }
            return R.fail(ResultCodeEnum.VALIDATE_FAILED.getCode(), fieldErrorMessageList.toString());
        }

输出

{
  "data": null,
  "code": 1002,
  "msg": "[字段:id 提示信息:最小值10 拒绝的值:1, 字段:id2 提示信息:最小值5 拒绝的值:2]"
}

分组校验

通用思想

后端思想篇:设计好接口的36个锦囊!

性能优化

7种方式,教你提升 SpringBoot 项目的吞吐量
Spring Boot 程序优化的 14 个小妙招!
实战总结!18种接口优化方案的总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值