网站开发速成,制作图片的软件加图免费,常州网络网站建设,怎么做网站的排名优化说到 Controller#xff0c;相信大家都不陌生#xff0c;它可以很方便地对外提供数据接口。它的定位#xff0c;我认为是「不可或缺的配角」#xff0c;说它不可或缺是因为无论是传统的三层架构还是现在的COLA架构#xff0c;Controller 层依旧有一席之地#xff0c;说明…说到 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) { thrownew 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; } thrownew 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) { thrownew 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 AllArgsConstructor public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { returnnew Result(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data); } public static T ResultT success(String message, T data) { returnnew Result(ResultEnum.SUCCESS.getCode(), message, data); } public static Result? failed() { retur nnew 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 ResultT instance(Integer code, String message, T data) { ResultT result new Result(); result.setCode(code); result.setMessage(message); result.setData(data); return result; } }统一返回结构后在 Controller 中就可以使用了但是每一个 Controller 都写这么一段最终封装的逻辑这些都是很重复的工作所以还要继续想办法进一步处理统一返回结构统一包装处理Spring 中提供了一个类ResponseBodyAdvice能帮助我们实现上述需求ResponseBodyAdvice是对 Controller 返回的内容在HttpMessageConverter进行类型转换之前拦截进行相应的处理操作后再将结果返回给客户端。那这样就可以把统一包装的工作放到这个类里面。public interface ResponseBodyAdviceT { 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 ResponseBodyAdviceObject { Override public boolean supports(MethodParameter returnType, Class? extends HttpMessageConverter? converterType) { // 如果不需要进行封装的可以添加一些校验手段比如添加标记排除的注解 returntrue; } 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.MappingJackson2HttpMessageConverterString 类型其他类型 (如 Integer 类型)现在问题已经较为清晰了因为我们需要返回一个Result对象所以使用MappingJackson2HttpMessageConverter是可以正常转换的而使用StringHttpMessageConverter字符串转换器会导致类型转换失败现在处理这个问题有两种方式在beforeBodyWrite方法处进行判断如果返回值是 String 类型就对Result对象手动进行转换成 JSON 字符串另外方便前端使用最好在RequestMapping中指定 ContentTypeRestControllerAdvice(basePackages com.example.demo) public class ResponseAdvice implements ResponseBodyAdviceObject { ... 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) { thrownew RuntimeException(e); } } return Result.success(body); } ... } GetMapping(value /returnString, produces application/json; charsetUTF-8) public String returnString() { returnsuccess; }修改HttpMessageConverter实例集合中MappingJackson2HttpMessageConverter的顺序。因为发生上述问题的根源所在是集合中StringHttpMessageConverter的顺序先于MappingJackson2HttpMessageConverter的调整顺序后即可从根源上解决这个问题网上有不少做法是直接在集合中第一位添加MappingJackson2HttpMessageConverterConfiguration public class WebConfiguration implements WebMvcConfigurer { Override public void configureMessageConverters(ListHttpMessageConverter? converters) { converters.add(0, new MappingJackson2HttpMessageConverter()); } }诚然这种方式可以解决问题但其实问题的根源不是集合中缺少这一个转换器而是转换器的顺序导致的所以最合理的做法应该是调整MappingJackson2HttpMessageConverter在集合中的顺序Configuration public class WebMvcConfiguration implements WebMvcConfigurer { /** * 交换MappingJackson2HttpMessageConverter与第一位元素 * 让返回值类型为String的接口能正常返回包装结果 * * param converters initially an empty list of converters */ Override public void configureMessageConverters(ListHttpMessageConverter? 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 validationspring validation是对其的二次封装常用于 SpringMVC 的参数自动校验参数校验的代码就不需要再与业务逻辑代码进行耦合了PathVariable 和 RequestParam 参数校验Get 请求的参数接收一般依赖这两个注解但是处于 url 有长度限制和代码的可维护性超过 5 个参数尽量用实体来传参对 PathVariable 和 RequestParam 参数进行校验需要在入参声明约束的注解如果校验失败会抛出MethodArgumentNotValidException异常RestController(value prettyTestController) RequestMapping(/pretty) Validated 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; } Autowired public void setTestService(TestService prettyTestService) { this.testService prettyTestService; } }校验原理在 SpringMVC 中有一个类是RequestResponseBodyMethodProcessor这个类有两个作用实际上可以从名字上得到一点启发用于解析 RequestBody 标注的参数处理 ResponseBody 标注方法的返回值解析 RequestBoyd 标注参数的方法是resolveArgumentpublic 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)) { thrownew MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer ! null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); } } publicabstractclass AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { /** * Validate the binding target if applicable. * pThe default implementation checks for {code javax.validation.Valid}, * Springs {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 publicclass TestDTO { NotBlank private String userName; NotBlank Length(min 6, max 20) private String password; NotNull Email private 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); } Autowired public 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; Override public 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 { Override public 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(); SetConstraintViolationObject result; try { //方法入参校验最终还是委托给Hibernate Validator来校验 //所以Spring Validation是对Hibernate Validation的二次封装 result execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { ... } //校验不通过抛出ConstraintViolationException异常 if (!result.isEmpty()) { thrownew ConstraintViolationException(result); } //Controller方法调用 Object returnValue invocation.proceed(); //下面是对返回值做校验流程和上面大概一样 result execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); if (!result.isEmpty()) { thrownew ConstraintViolationException(result); } return returnValue; } }自定义校验规则有些时候JSR303标准中提供的校验规则不满足复杂的业务需求也可以自定义校验规则自定义校验规则需要做两件事情自定义注解类定义错误信息和一些其他需要的内容注解校验器定义判定规则//自定义注解类 Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) Retention(RetentionPolicy.RUNTIME) Documented Constraint(validatedBy MobileValidator.class) public interface Mobile { /** * 是否允许为空 */ boolean required() default true; /** * 校验不通过返回的提示信息 */ String message() default 不是一个手机号码格式; /** * Constraint要求的属性用于分组校验和扩展留空就好 */ Class?[] groups() default {}; Class? extends Payload[] payload() default {}; } //注解校验器 public class MobileValidator implements ConstraintValidatorMobile, CharSequence { private boolean required false; private final Pattern pattern Pattern.compile(^1[34578][0-9]{9}$); // 验证手机号 /** * 在验证开始前调用注解里的方法从而获取到一些注解里的参数 * * param constraintAnnotation annotation instance for a given constraint declaration */ Override public void initialize(Mobile constraintAnnotation) { this.required constraintAnnotation.required(); } /** * 判断参数是否合法 * * param value object to validate * param context context in which the constraint is evaluated */ Override public boolean isValid(CharSequence value, ConstraintValidatorContext context) { if (this.required) { // 验证 return isMobile(value); } if (StringUtils.hasText(value)) { // 验证 return isMobile(value); } returntrue; } private boolean isMobile(final CharSequence str) { Matcher m pattern.matcher(str); return m.matches(); } }自动校验参数真的是一项非常必要、非常有意义的工作。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 方法返回的是什么数据也可以方便每一个异常应该如何进行反馈这一套操作下来后我们能更加专注于业务逻辑的开发代码简洁、功能完善何乐而不为呢作者gelald来源juejin.cn/post/7123091045071454238—END—好了给粉丝个福利打车千万别直接去APP记得先领券 亲测一单省了30块钱