Controller 层代码就该这么写,简洁又优雅!

一个优秀的 Controller 层逻辑

说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口。它的定位,我认为是「不可或缺的配角」。

说它不可或缺是因为无论是传统的三层架构还是现在的 COLA 架构,Controller 层依旧有一席之地,说明他的必要性。

说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求。

从现状看问题

 Controller 主要的工作有以下几项:
=>接收请求并解析参数
=>调用 Service 执行具体的业务代码(可能包含参数校验)
=>捕获业务逻辑异常做出反馈
=>业务逻辑执行成功做出响应

//DTO
@Data
public class TestDTO {private Integer num;private String type;
}//Service
@Service
public 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
@RestController
public 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);}}@Autowiredpublic DTOid setTestService(TestService testService) {this.testService = testService;}
}

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

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

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

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

改造 Controller 层逻辑

统一返回结构

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

推荐一个开源免费的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

使用一个状态码、状态信息就能清楚地了解接口调用情况:

//定义返回数据结构
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
@AllArgsConstructor
public class Result<T> {private Integer code;private String message;private T data;public static <T> Result<T> success(T data) {return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);}public static <T> Result<T> 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 <T> Result<T> instance(Integer code, String message, T data) {Result<T> result = new Result<>();result.setCode(code);result.setMessage(message);result.setData(data);return result;}
}

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

统一包装处理

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

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

ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。

那这样就可以把统一包装的工作放到这个类里面:

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

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

// 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {// 如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解return true;}@Overridepublic 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 返回的数据进行统一包装,又不需要对原有代码进行大量的改动。

另外,如果你近期准备面试跳槽,建议在Java面试库小程序在线刷题,涵盖 2000+ 道 Java 面试题,几乎覆盖了所有主流技术面试题。

参数校验

Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。

spring validation 是对其的二次封装,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了。

①@PathVariable 和 @RequestParam 参数校验

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

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

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

@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public 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;}@Autowiredpublic void setTestService(TestService prettyTestService) {this.testService = prettyTestService;}
}
校验原理

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

  • 用于解析 @RequestBody 标注的参数

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

解析 @RequestBody 标注参数的方法是 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.*/@Overridepublic 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 {/*** Validate the binding target if applicable.* <p>The default implementation checks for {@code @javax.validation.Valid},* Spring's {@link org.springframework.validation.annotation.Validated},* and custom annotations whose name starts with "Valid".* @param binder the DataBinder to be used* @param parameter the method parameter descriptor* @since 4.1.5* @see #isBindExceptionRequired*/protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {//获取参数上的所有注解Annotation[] annotations = parameter.getParameterAnnotations();for (Annotation ann : annotations) {//如果注解中包含了@Valid、@Validated或者是名字以Valid开头的注解就进行参数校验Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);if (validationHints != null) {//实际校验逻辑,最终会调用Hibernate Validator执行真正的校验//所以Spring Validation是对Hibernate Validation的二次封装binder.validate(validationHints);break;}}}
}

 

 
②@RequestBody 参数校验

Post、Put 请求的参数推荐使用 @RequestBody 请求体参数。

对 @RequestBody 参数进行校验需要在 DTO 对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。

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

//DTO
@Data
public class TestDTO {@NotBlankprivate String userName;@NotBlank@Length(min = 6, max = 20)private String password;@NotNull@Emailprivate String email;
}//Controller
@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public class TestController {private TestService testService;@PostMapping("/test-validation")public void testValidation(@RequestBody @Validated TestDTO testDTO) {this.testService.save(testDTO);}@Autowiredpublic void setTestService(TestService testService) {this.testService = testService;}
}
校验原理

声明约束的方式,注解加到了参数上面,可以比较容易猜测到是使用了 AOP 对方法进行增强。

而实际上 Spring 也是通过 MethodValidationPostProcessor 动态注册 AOP 切面,然后使用 MethodValidationInterceptor 对切点方法进行织入增强。

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {//指定了创建切面的Bean的注解private Class<? extends Annotation> validatedAnnotationType = Validated.class;@Overridepublic void afterPropertiesSet() {//为所有@Validated标注的Bean创建切面Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);//创建Advisor进行增强this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));}//创建Advice,本质就是一个方法拦截器protected Advice createMethodValidationAdvice(@Nullable Validator validator) {return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());}
}public class MethodValidationInterceptor implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {//无需增强的方法,直接跳过if (isFactoryBeanMetadataMethod(invocation.getMethod())) {return invocation.proceed();}Class<?>[] groups = determineValidationGroups(invocation);ExecutableValidator execVal = this.validator.forExecutables();Method methodToValidate = invocation.getMethod();Set<ConstraintViolation<Object>> result;try {//方法入参校验,最终还是委托给Hibernate Validator来校验//所以Spring Validation是对Hibernate Validation的二次封装result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);}catch (IllegalArgumentException ex) {...}//校验不通过抛出ConstraintViolationException异常if (!result.isEmpty()) {throw new ConstraintViolationException(result);}//Controller方法调用Object returnValue = invocation.proceed();//下面是对返回值做校验,流程和上面大概一样result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);if (!result.isEmpty()) {throw new ConstraintViolationException(result);}return returnValue;}
}
③自定义校验规则

有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则。有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则。

自定义校验规则需要做两件事情: 

自定义校验规则需要做两件事情:

自动校验参数真的是一项非常必要、非常有意义的工作。JSR303 提供了丰富的参数校验规则,再加上复杂业务的自定义校验规则,完全把参数校验和业务逻辑解耦开,代码更加简洁,符合单一职责原则。

自定义异常与统一拦截异常

原来的代码中可以看到有几个问题:

​=>抛出的异常不够具体,只是简单地把错误信息放到了 Exception 中
​=>抛出异常后,Controller 不能具体地根据异常做出反馈
​=>虽然做了参数自动校验,但是异常返回结构和正常返回结构不一致

自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应。

而统一拦截异常的目的一个是为了可以与前面定义下来的统一包装返回结构能对应上,另一个是我们希望无论系统发生什么异常,Http 的状态码都要是 200 ,尽可能由业务来区分系统的异常。

//自定义异常
public class ForbiddenException extends RuntimeException {public ForbiddenException(String message) {super(message);}
}//自定义异常
public class BusinessException extends RuntimeException {public BusinessException(String message) {super(message);}
}//统一拦截异常
@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {/*** 捕获 {@code BusinessException} 异常*/@ExceptionHandler({BusinessException.class})public Result<?> handleBusinessException(BusinessException ex) {return Result.failed(ex.getMessage());}/*** 捕获 {@code ForbiddenException} 异常*/@ExceptionHandler({ForbiddenException.class})public Result<?> handleForbiddenException(ForbiddenException ex) {return Result.failed(ResultEnum.FORBIDDEN);}/*** {@code @RequestBody} 参数校验不通过时抛出的异常处理*/@ExceptionHandler({MethodArgumentNotValidException.class})public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {BindingResult bindingResult = ex.getBindingResult();StringBuilder sb = new StringBuilder("校验失败:");for (FieldError fieldError : bindingResult.getFieldErrors()) {sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");}String msg = sb.toString();if (StringUtils.hasText(msg)) {return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);}return Result.failed(ResultEnum.VALIDATE_FAILED);}/*** {@code @PathVariable} 和 {@code @RequestParam} 参数校验不通过时抛出的异常处理*/@ExceptionHandler({ConstraintViolationException.class})public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {if (StringUtils.hasText(ex.getMessage())) {return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());}return Result.failed(ResultEnum.VALIDATE_FAILED);}/*** 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用*/@ExceptionHandler({Exception.class})public Result<?> handle(Exception ex) {return Result.failed(ex.getMessage());}}

总结

做好了这一切改动后,可以发现 Controller 的代码变得非常简洁,可以很清楚地知道每一个参数、每一个 DTO 的校验规则,可以很明确地看到每一个 Controller 方法返回的是什么数据,也可以方便每一个异常应该如何进行反馈。

这一套操作下来后,我们能更加专注于业务逻辑的开发,代码简介、功能完善,何乐而不为呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/173490.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DEM分析

一、实验名称&#xff1a; DEM分析 二、实验目的&#xff1a; 通过本实验练习&#xff0c;掌握DEM的建立与应用基本方法。 三、实验内容和要求&#xff1a; 实验内容&#xff1a; 利用ARCGIS软件相关分析工具及实验数据&#xff0c;创建DEM&#xff0c;并计算相应坡度的区…

webshell之编码免杀

Unicode编码 jsp支持unicode编码&#xff0c;如果杀软不支持unicode查杀的话&#xff0c;基本上都能绕过 注意这里的\uuuu00可以换成\uuuu00uuu...可以跟多个u达到绕过的效果 将代码&#xff08;除page以及标签&#xff09;进行unicode编码&#xff0c;并条件到<%%>标签…

sCrypt 在英国伦敦 Exeter 大学讲学

6月5日&#xff0c;sCrypt CEO晓晖和他的两位同事在英国伦敦Exeter大学举行了一场精彩的讲座。刘晓晖向听众们详细介绍了sCrypt智能合约开平台&#xff0c;并演示了如何使用sCrypt来开发基于比特币的智能合约。他用生动形象的语言&#xff0c;深入浅出地解释了这个领域复杂而又…

16、单例bean的优势

一、单例bean的优势 由于不会每次都新创建新对象所以有一下几个性能上的优势&#xff1a; 减少了新生成实例的消耗。新生成实例消耗包括两方面&#xff0c;第一&#xff0c;spring会通过反射或者cglib来生成bean实例&#xff0c;这都是耗性能的操作&#xff0c;其次给对象分配…

精通Nginx(18)-FastCGI/SCGI/uWSGI支持

最初用浏览器浏览的网页只能是静态html页面。随着社会发展,动态获取数据、操作数据需要变得日益强烈,CGI应运而生。CGI(Common Gateway Interface)公共网关接口,是外部扩展应用程序与静态Web服务器交互的一个标准接口。它可以使外部程序处理浏览器送来的表单数据并对此作出…

24. 深度学习进阶 - 矩阵运算的维度和激活函数

Hi&#xff0c;你好。我是茶桁。 咱们经过前一轮的学习&#xff0c;已经完成了一个小型的神经网络框架。但是这也只是个开始而已&#xff0c;在之后的课程中&#xff0c;针对深度学习我们需要进阶学习。 我们要学到超参数&#xff0c;优化器&#xff0c;卷积神经网络等等。看…

17.Spring实例化bean方式的几种方式

Spring实例化bean方式的几种方式 构造器方式&#xff08;反射&#xff09;&#xff1b; 静态工厂方式&#xff1b; factory-method 实例工厂方式(Bean)&#xff1b; factory-beanfactory-method FactoryBean方式 Spring 三种实例化 bean 的方式比较 方式一&#xff1a;通…

C++封装dll和lib 供C++调用

头文件interface.h #pragma once #ifndef INTERFACE_H #define INTERFACE_H #define _CRT_SECURE_NO_WARNINGS #define FENGZHUANG_API _declspec(dllexport) #include <string> namespace FengZhuang {class FENGZHUANG_API IInterface {public:static IInterface* Cre…

Node——npm包管理器的使用

Node.js使用npm对包进行管理&#xff0c;其全称为Node Package Manager&#xff0c;开发人员可以使用它安装、更新或者卸载Node.js的模块 1、npm包管理器基础 1.1、npm概述 npm是Node.js的标准软件包管理器&#xff0c;其在2020年3月17日被GitHub收购&#xff0c;而且保证永…

机器学习笔记 - 3D对象检测技术路线调研(未完)

一、3D对象检测简述 3D对象检测是计算机视觉中的一项任务&#xff0c;其目标是根据对象的形状、位置和方向在 3D 环境中识别和定位对象。它涉及检测物体的存在并实时确定它们在 3D 空间中的位置。这项任务对于自动驾驶汽车、机器人和增强现实等应用至关重要。 1、基本流程 给定…

虾皮买手号怎么弄的

想要拥有虾皮买手号&#xff0c;可以使用shopee买家通系统进行自动化注册&#xff0c;这款软件目前支持菲律宾、泰国、马来西亚、越南、巴西、印度尼西亚等国家使用。 软件注册流程简单方便&#xff0c;首先我们需要先准备好手机号&#xff0c;因为现在注册虾皮买家号基本上都是…

UI自动化的基本知识

一、UI自动化测试介绍 1、什么是自动化测试 概念&#xff1a;由程序代替人工进行系统校验的过程 1.1自动化测试能解决的问题&#xff1f; 回归测试 (冒烟测试) 针对之前老的功能进行测试 通过自动化的代码来实现。 针对上一个版本的问题的回归 兼容性测试 web实例化不同的浏…

C语言基础篇5:指针(一)

指针是C语言的核心、精髓所在&#xff0c;用好了指针可以在C语言编程中起到事半功倍的效果。指针一方面可以提高程序的编译效率和执行速度&#xff0c;而且还可以通过指针实现动态的存储分配&#xff0c;另一方面使用指针可使程序更灵活&#xff0c;便于表示各种数据结构&#…

力扣labuladong——一刷day57

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、力扣1379. 找出克隆二叉树中的相同节点二、力扣LCR 143. 子结构判断三、力扣110. 平衡二叉树四、力扣250. 统计同值子树 前言 有些题目&#xff0c;你按照拍…

LeetCode Hot100 236.二叉树的最近公共祖先

题目&#xff1a; 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点 p、q&#xff0c;最近公共祖先表示为一个节点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽可能大&#xff08;一个节…

HT97220与HT97230耳机放大器芯片对比

HT97230有两个不同开启时间(tON)版本&#xff0c;版本A、C和E的导通时间tON为5.5ms&#xff0c;用于耳机驱动&#xff1b;B和D则具有130ms的tON&#xff0c;用于机顶盒设计&#xff08;目前仅提供A版本&#xff0c;其他版本需预定&#xff09;。内部电荷泵对输入电源反相&#…

05 在C++中,什么是变量?变量有哪些类型?

系列文章目录 在C中&#xff0c;什么是变量&#xff1f;变量有哪些类型&#xff1f; 目录 系列文章目录 文章目录 前言 一、C中的变量 1.什么是变量&#xff1f; 2.变量的类型 二、C 中的变量定义和声明 1.变量定义 2.变量声明 三、扩展 1.函数 2.自动转换规则 总…

accelerate的使用说明

1 多卡(GPU)使用方法 终端输入指令&#xff0c;生成问答页面 accelerate config 这个方法也是可以的 2 后面修改直接找到这个yaml文件进行修改即可 cd ~/.cache/huggingface/accelerate vim default_config.yaml 进入vim进行修改 3 单卡(GPU)使用方法 vim default_config.…

什么是SQL?

SQL和MySQL是当今计算机领域中非常重要的两个概念。SQL是关系型数据库的查询语言&#xff0c;而MySQL是一种关系型数据库管理系统。它们在数据存储、管理和查询方面发挥着巨大的作用。在本文中&#xff0c;我们将深入探讨SQL和MySQL的定义、功能、应用以及它们之间的联系。 一…

边缘计算网关:智能制造的“智慧大脑”

一、智能制造的崛起 随着科技的飞速发展&#xff0c;智能制造已经成为了制造业的新趋势。智能制造不仅能够提高生产效率&#xff0c;降低生产成本&#xff0c;还能够实现个性化定制&#xff0c;满足消费者多样化的需求。然而&#xff0c;智能制造的实现离不开大量的数据处理和分…