专栏名称: 架构师
架构师云集,三高架构(高可用、高性能、高稳定)、大数据、机器学习、Java架构、系统架构、大规模分布式架构、人工智能等的架构讨论交流,以及结合互联网技术的架构调整,大规模架构实战分享。欢迎有想法、乐于分享的架构师交流学习。
目录
相关文章推荐
算法爱好者  ·  董事长砍死 ... ·  昨天  
人工智能与大数据技术  ·  DeepSeek一天能赚多少钱?官方突然揭秘 ... ·  2 天前  
大数据文摘  ·  AI超级碗!英伟达GTC大会宣布Blackw ... ·  2 天前  
大数据分析和人工智能  ·  告别996,DeepSeek真的非常强大! ·  3 天前  
51好读  ›  专栏  ›  架构师

Controller层代码这么写,任督二脉全打通

架构师  · 公众号  ·  · 2025-01-21 22:28

正文

架构师(JiaGouX)
我们都是架构师!
架构未来,你来不来?



说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口。它的定位,我认为是「不可或缺的配角」,说它不可或缺是因为无论是传统的三层架构还是现在的COLA架构,Controller 层依旧有一席之地,说明他的必要性;说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求。



一、从现状看问题


Controller 主要的工作有以下几项:

  • 接收请求并解析参数

  • 调用 Service 执行具体的业务代码(可能包含参数校验)

  • 捕获业务逻辑异常做出反馈

  • 业务逻辑执行成功做出响应

//DTO@Datapublic class TestDTO {    private Integer num;    private String type;}

//Service@Servicepublic class TestService {
public Double service(TestDTO testDTO) throws Exception { if (testDTO.getNum() <= 0) { throw new Exception("输入的数字需要大于0"); } if (testDTO.getType().equals("square")) { return Math.pow(testDTO.getNum(), 2); } if (testDTO.getType().equals("factorial")) { double result = 1 ; int num = testDTO.getNum(); while (num > 1) { result = result * num; num -= 1; } return result; } throw new Exception("未识别的算法"); }}
//Controller@RestControllerpublic class TestController {
private TestService testService;
@PostMapping("/test") public Double test(@RequestBody TestDTO testDTO) { try { Double result = this.testService.service(testDTO); return result; } catch (Exception e) { throw new RuntimeException(e); } }
@Autowired public DTOid setTestService(TestService testService) { this.testService = testService; }}

如果真的按照上面所列的工作项来开发 Controller 代码会有几个问题:

  • 参数校验过多地耦合了业务代码,违背单一职责原则

  • 可能在多个业务中都抛出同一个异常,导致代码重复

  • 各种异常反馈和成功响应格式不统一,接口对接不友好



二、改造 Controller 层逻辑


统一返回结构


统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此),使用一个状态码、状态信息就能清楚地了解接口调用情况。


//定义返回数据结构




    
public interface IResult {    Integer getCode();    String getMessage();}
//常用结果的枚举public enum ResultEnum implements IResult { SUCCESS(2001, "接口调用成功"), VALIDATE_FAILED(2002, "参数校验失败"), COMMON_FAILED(2003, "接口调用失败"), FORBIDDEN(2004, "没有权限访问资源");
private Integer code; private String message; //省略get、set方法和构造方法}//统一返回数据结构@Data@NoArgsConstructor@AllArgsConstructorpublic class Result { private Integer code; private String message; private T data;
public static Result success(T data) { return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data); }
public static Result success(String message, T data) { return new Result<>(ResultEnum.SUCCESS.getCode(), message, data); }
public static Result> failed() { return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null); }
public static Result> failed(String message) { return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null); }
public static Result> failed(IResult errorResult) { return new Result<>(errorResult.getCode(), errorResult.getMessage(), null); }
public static Result instance(Integer code, String message, T data) { Result result = new Result<>(); result.setCode(code); result.setMessage(message); result.setData(data); return result; }}


统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构。


统一包装处理


Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求。


ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。那这样就可以把统一包装的工作放到这个类里面。


public interface ResponseBodyAdvice {    boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType);
@Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);}


supports:判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要

beforeBodyWrite:对 response 进行具体的处理。


// 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成@RestControllerAdvice(basePackages = "com.example.demo")public class ResponseAdvice implements ResponseBodyAdvice<Object> {    @Override    public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) {        // 如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解        return true;    }
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 提供一定的灵活度,如果body已经被包装了,就不进行包装 if (body instanceof Result) { return body; } return Result.success(body); }}


经过这样改造,既能实现对 Controller 返回的数据进行统一包装,又不需要对原有代码进行大量的改动。


处理 cannot be cast to java.lang.String 问题


如果直接使用 ResponseBodyAdvice,对于一般的类型都没有问题,当处理字符串类型时,会抛出 xxx.包装类 cannot be cast to java.lang.String 的类型转换的异常。


在 ResponseBodyAdvice 实现类中 debug 发现,只有 String 类型的 selectedConverterType 参数值是 org.springframework.http.converter.StringHttpMessageConverter,而其他数据类型的值是 org.springframework.http.converter.json.MappingJackson2HttpMessageConverter。


  • String 类型



  • 其他类型 (如 Integer 类型)



现在问题已经较为清晰了,因为我们需要返回一个 Result 对象。


所以使用 MappingJackson2HttpMessageConverter 是可以正常转换的。


而使用 StringHttpMessageConverter 字符串转换器会导致类型转换失败。


现在处理这个问题有两种方式:


①在 beforeBodyWrite 方法处进行判断,如果返回值是 String 类型就对 Result 对象手动进行转换成 JSON 字符串,另外方便前端使用,最好在 @RequestMapping 中指定 ContentType。


@RestControllerAdvice(basePackages = "com.example.demo")public class ResponseAdvice implements ResponseBodyAdvice<Object> {    ...    @Override    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {        // 提供一定的灵活度,如果body已经被包装了,就不进行包装        if (body instanceof Result) {            return body;        }        // 如果返回值是String类型,那就手动把Result对象转换成JSON字符串        if (body instanceof String) {            try {                return this.objectMapper.writeValueAsString(Result.success(body));            } catch (JsonProcessingException e) {                throw new RuntimeException(e);            }        }        return Result.success(body);    }    ...}

@GetMapping(value = "/returnString", produces = "application/json; charset=UTF-8")public String returnString() { return "success";}


②修改 HttpMessageConverter 实例集合中 MappingJackson2HttpMessageConverter 的顺序。因为发生上述问题的根源所在是集合中 StringHttpMessageConverter 的顺序先于 MappingJackson2HttpMessageConverter 的,调整顺序后即可从根源上解决这个问题。


网上有不少做法是直接在集合中第一位添加 MappingJackson2HttpMessageConverter。


@Configurationpublic class WebConfiguration implements WebMvcConfigurer {        @Override    public void configureMessageConverters(List> converters) {        converters.add(0, new MappingJackson2HttpMessageConverter());    }}


诚然,这种方式可以解决问题,但其实问题的根源不是集合中缺少这一个转换器,而是转换器的顺序导致的,所以最合理的做法应该是调整 MappingJackson2HttpMessageConverter 在集合中的顺序。


@Configurationpublic class WebMvcConfiguration implements WebMvcConfigurer {
/** * 交换MappingJackson2HttpMessageConverter与第一位元素 * 让返回值类型为String的接口能正常返回包装结果 * * @param converters initially an empty list of converters */ @Override public void configureMessageConverters(List> converters) { for (int i = 0; i < converters.size(); i++) { if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) { MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = (MappingJackson2HttpMessageConverter) converters.get(i); converters.set(i, converters.get(0)); converters.set(0, mappingJackson2HttpMessageConverter); break; } } }}


参数校验


Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation ,spring validation 是对其的二次封装,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了。


@PathVariable 和 @RequestParam 参数校验


Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。


对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。


如果校验失败,会抛出 MethodArgumentNotValidException 异常。


@RestController(value = "prettyTestController")@RequestMapping("/pretty")@Validatedpublic class TestController {
private TestService testService;
@GetMapping("/{num}") public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) { return num * num; }
@GetMapping("/getByEmail") public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) { TestDTO testDTO = new TestDTO(); testDTO.setEmail(email); return testDTO; }
@Autowired public void setTestService(TestService prettyTestService) { this.testService = prettyTestService; }}


校验原理


在 SpringMVC 中,有一个类是 RequestResponseBodyMethodProcessor ,这个类有两个作用(实际上可以从名字上得到一点启发)。


用于解析 @RequestBody 标注的参数

处理 @ResponseBody 标注方法的返回值


解析 @RequestBoyd 标注参数的方法是 resolveArgument。


public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {      /**     * Throws MethodArgumentNotValidException if validation fails.     * @throws HttpMessageNotReadableException if {@link RequestBody#required()}     * is {@code true} and there is no body content or if there is no suitable     * converter to read the content with.     */    @Override    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional(); //把请求数据封装成标注的DTO对象 Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter);

if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { //执行数据校验 validateIfApplicable(binder, parameter); //如果校验不通过,就抛出MethodArgumentNotValidException异常 //如果我们不自己捕获,那么最终会由DefaultHandlerExceptionResolver捕获处理 if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } }

return adaptArgumentIfNecessary(arg, parameter); }}

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver






请到「今天看啥」查看全文