温州大军建设有限公司网站,wordpress内部优化,好的网站设计模板,展示照片的网站前言本篇主要要介绍的就是controller层的处理#xff0c;一个完整的后端请求由4部分组成#xff1a;1. 接口地址(也就是URL地址)、2. 请求方式(一般就是get、set#xff0c;当然还有put、delete)、3. 请求数据(request#xff0c;有head跟body)、4. 响应数据(response)本篇…前言本篇主要要介绍的就是controller层的处理一个完整的后端请求由4部分组成1. 接口地址(也就是URL地址)、2. 请求方式(一般就是get、set当然还有put、delete)、3. 请求数据(request有head跟body)、4. 响应数据(response)本篇将解决以下3个问题当接收到请求时如何优雅的校验参数返回响应数据该如何统一的进行处理接收到请求处理业务逻辑时抛出了异常又该如何处理一、Controller层参数接收常见的请求就分为get跟post2种
RestController
RequestMapping(/product/product-info)
public class ProductInfoController {AutowiredProductInfoService productInfoService;GetMapping(/findById)public ProductInfoQueryVo findById(Integer id) {...}PostMapping(/page)public IPage findPage(Page page, ProductInfoQueryVo vo) {...}
}RestController 之前解释过RestController Controller ResponseBody。加上这个注解springboot就会吧这个类当成controller进行处理然后把所有返回的参数放到ResponseBody中RequestMapping 请求的前缀也就是所有该Controller下的请求都需要加上/product/product-info的前缀GetMapping(/findById) 标志这是一个get请求并且需要通过/findById地址才可以访问到PostMapping(/page) 同理表示是个post请求参数 至于参数部分只需要写上ProductInfoQueryVo前端过来的json请求便会通过映射赋值到对应的对象中例如请求这么写productId就会自动被映射到vo对应的属性当中size : 1
current : 1
productId : 1
productName : 泡脚二、统一状态码1. 返回格式为了跟前端妹妹打好关系我们通常需要对后端返回的数据进行包装一下增加一下状态码状态信息这样前端妹妹接收到数据就可以根据不同的状态码判断响应数据状态是否成功是否异常进行不同的显示。当然这让你拥有了更多跟前端妹妹的交流机会假设我们约定了1000就是成功的意思如果你不封装那么返回的数据是这样子的{productId: 1,productName: 泡脚,productPrice: 100.00,productDescription: 中药泡脚加按摩,productStatus: 0,
}经过封装以后时这样子的{code: 1000,msg: 请求成功,data: {productId: 1,productName: 泡脚,productPrice: 100.00,productDescription: 中药泡脚加按摩,productStatus: 0,}
}2. 封装ResultVo这些状态码肯定都是要预先编好的怎么编呢写个常量1000还是直接写死1000要这么写就真的书白读的了写状态码当然是用枚举拉首先先定义一个状态码的接口所有状态码都需要实现它有了标准才好做事public interface StatusCode {public int getCode();public String getMsg();
}然后去找前端妹妹跟他约定好状态码这可能是你们唯一的约定了枚举类嘛当然不能有setter方法了因此我们不能在用Data注解了我们要用GetterGetter
public enum ResultCode implements StatusCode{SUCCESS(1000, 请求成功),FAILED(1001, 请求失败),VALIDATE_ERROR(1002, 参数校验失败),RESPONSE_PACK_ERROR(1003, response返回包装失败);private int code;private String msg;ResultCode(int code, String msg) {this.code code;this.msg msg;}
}写好枚举类开始写ResultVo包装类了我们预设了几种默认的方法比如成功的话就默认传入object就可以了我们自动包装成success Data
public class ResultVo {// 状态码private int code;// 状态信息private String msg;// 返回对象private Object data;// 手动设置返回vopublic ResultVo(int code, String msg, Object data) {this.code code;this.msg msg;this.data data;}// 默认返回成功状态码数据对象public ResultVo(Object data) {this.code ResultCode.SUCCESS.getCode();this.msg ResultCode.SUCCESS.getMsg();this.data data;}// 返回指定状态码数据对象public ResultVo(StatusCode statusCode, Object data) {this.code statusCode.getCode();this.msg statusCode.getMsg();this.data data;}// 只返回状态码public ResultVo(StatusCode statusCode) {this.code statusCode.getCode();this.msg statusCode.getMsg();this.data null;}
}使用现在的返回肯定就不是return data;这么简单了而是需要new ResultVo(data);PostMapping(/findByVo)
public ResultVo findByVo(Validated ProductInfoVo vo) {ProductInfo productInfo new ProductInfo();BeanUtils.copyProperties(vo, productInfo);return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}最后返回就会是上面带了状态码的数据了三、统一校验1. 原始做法假设有一个添加ProductInfo的接口在没有统一校验时我们需要这么做Data
public class ProductInfoVo {// 商品名称private String productName;// 商品价格private BigDecimal productPrice;// 上架状态private Integer productStatus;
}
PostMapping(/findByVo)
public ProductInfo findByVo(ProductInfoVo vo) {if (StringUtils.isNotBlank(vo.getProductName())) {throw new APIException(商品名称不能为空);}if (null ! vo.getProductPrice() vo.getProductPrice().compareTo(new BigDecimal(0)) 0) {throw new APIException(商品价格不能为负数);}...ProductInfo productInfo new ProductInfo();BeanUtils.copyProperties(vo, productInfo);return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}这if写的人都傻了能忍吗肯定不能忍啊2. Validated参数校验好在有Validated又是一个校验参数必备良药了。有了Validated我们只需要再vo上面加一点小小的注解便可以完成校验功能Data
public class ProductInfoVo {NotNull(message 商品名称不允许为空)private String productName;Min(value 0, message 商品价格不允许为负数)private BigDecimal productPrice;private Integer productStatus;
}
PostMapping(/findByVo)
public ProductInfo findByVo(Validated ProductInfoVo vo) {ProductInfo productInfo new ProductInfo();BeanUtils.copyProperties(vo, productInfo);return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}
运行看看如果参数不对会发生什么
我们故意传一个价格为-1的参数过去
productName : 泡脚
productPrice : -1
productStatus : 1
{timestamp: 2020-04-19T03:06:37.2680000,status: 400,error: Bad Request,errors: [{codes: [Min.productInfoVo.productPrice,Min.productPrice,Min.java.math.BigDecimal,Min],arguments: [{codes: [productInfoVo.productPrice,productPrice],defaultMessage: productPrice,code: productPrice},0],defaultMessage: 商品价格不允许为负数,objectName: productInfoVo,field: productPrice,rejectedValue: -1,bindingFailure: false,code: Min}],message: Validation failed for object\u003d\u0027productInfoVo\u0027. Error count: 1,trace: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object \u0027productInfoVo\u0027 on field \u0027productPrice\u0027: rejected value [-1]; codes [Min.productInfoVo.productPrice,Min.productPrice,Min.java.math.BigDecimal,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [productInfoVo.productPrice,productPrice]; arguments []; default message [productPrice],0]; default message [商品价格不允许为负数]\n\tat org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:164)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:660)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:124)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1594)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:830)\n,path: /leilema/product/product-info/findByVo
}大功告成了吗虽然成功校验了参数也返回了异常并且带上商品价格不允许为负数的信息。但是你要是这样返回给前端前端妹妹就提刀过来了当年约定好的状态码你个负心人说忘就忘用户体验小于等于0啊所以我们要进行优化一下每次出现异常的时候自动把状态码写好不负妹妹之约拓展关于参数的验证 具体参考 Springboot整合JSR303参数校验3. 优化异常处理首先我们先看看校验参数抛出了什么异常Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors我们看到代码抛出了org.springframework.validation.BindException的绑定异常因此我们的思路就是AOP拦截所有controller然后异常的时候统一拦截起来进行封装完美玩你个头啊完美这么呆瓜的操作springboot不知道吗spring mvc当然知道拉所以给我们提供了一个RestControllerAdvice来增强所有RestController然后使用ExceptionHandler注解就可以拦截到对应的异常。这里我们就拦截BindException.class就好了。最后在返回之前我们对异常信息进行包装一下包装成ResultVo当然要跟上ResultCode.VALIDATE_ERROR的异常状态码。这样前端妹妹看到VALIDATE_ERROR的状态码就会调用数据校验异常的弹窗提示用户哪里没填好RestControllerAdvice
public class ControllerExceptionAdvice {ExceptionHandler({BindException.class})public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {// 从异常对象中拿到ObjectError对象ObjectError objectError e.getBindingResult().getAllErrors().get(0);return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());}
}来康康效果完美。1002与前端妹妹约定好的状态码{code: 1002,msg: 参数校验失败,data: 商品价格不允许为负数
} 这里可以以上的枚举可以规定好不同的业务模块对应的编号大型系统一般有指定生成的不同的业务异常分不同的枚举类来规定不同的业务异常信息规范很重要尤其是对外的api良好的规范利于系统后期维护和迭代有利于bug的定位和查找。 当然这是全局异常的处理方式从全局的角度来 在局部上指定捕获业务异常的方式也用户提示 以下为业务异常的类BusinessException 通过抛业对应的业务异常来给用户提示。将错误信息置于message中。package com.bzcst.bop.common.exception;public class BusinessException extends RuntimeException {private int code;private String msg;public BusinessException(String msg) {super(msg);this.code 1;this.msg msg;}public BusinessException(int code, String msg) {super(msg);this.code code;this.msg msg;}public BusinessException(String msg, Throwable cause) {super(msg, cause);this.code 1;this.msg msg;}public BusinessException(int code, String msg, Throwable cause) {super(msg, cause);this.code code;this.msg msg;}public BusinessException(ErrorEnum errorEnum) {super(errorEnum.message);this.code errorEnum.errorCode;this.msg errorEnum.message;}public BusinessException(ErrorEnum errorEnum, Throwable cause) {super(errorEnum.message, cause);this.code errorEnum.errorCode;this.msg errorEnum.message;}public int getCode() {return this.code;}public String getMsg() {return this.msg;}public void setCode(final int code) {this.code code;}public void setMsg(final String msg) {this.msg msg;}public boolean equals(final Object o) {if (o this) {return true;} else if (!(o instanceof BusinessException)) {return false;} else {BusinessException other (BusinessException)o;if (!other.canEqual(this)) {return false;} else if (this.getCode() ! other.getCode()) {return false;} else {Object this$msg this.getMsg();Object other$msg other.getMsg();if (this$msg null) {if (other$msg ! null) {return false;}} else if (!this$msg.equals(other$msg)) {return false;}return true;}}}protected boolean canEqual(final Object other) {return other instanceof BusinessException;}public int hashCode() {int PRIME true;int result 1;int result result * 59 this.getCode();Object $msg this.getMsg();result result * 59 ($msg null ? 43 : $msg.hashCode());return result;}public String toString() {return BusinessException(code this.getCode() , msg this.getMsg() );}
}四、统一响应1. 统一包装响应再回头看一下controller层的返回return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));开发小哥肯定不乐意了谁有空天天写new ResultVo(data)啊我就想返回一个实体怎么实现我不管好把那就是AOP拦截所有Controller再After的时候统一帮你封装一下咯怕是上一次脸打的不够疼springboot能不知道这么个操作吗RestControllerAdvice(basePackages {com.bugpool.leilema})
public class ControllerResponseAdvice implements ResponseBodyAdviceObject {Overridepublic boolean supports(MethodParameter methodParameter, Class? extends HttpMessageConverter? aClass) {// response是ResultVo类型或者注释了NotControllerResponseAdvice都不进行包装return !methodParameter.getParameterType().isAssignableFrom(ResultVo.class);}Overridepublic Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class? extends HttpMessageConverter? aClass, ServerHttpRequest request, ServerHttpResponse response) {// String类型不能直接包装if (returnType.getGenericParameterType().equals(String.class)) {ObjectMapper objectMapper new ObjectMapper();try {// 将数据包装在ResultVo里后转换为json串进行返回return objectMapper.writeValueAsString(new ResultVo(data));} catch (JsonProcessingException e) {throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());}}// 否则直接包装成ResultVo返回return new ResultVo(data);}
}RestControllerAdvice(basePackages {com.bugpool.leilema})自动扫描了所有指定包下的controller在Response时进行统一处理重写supports方法也就是说当返回类型已经是ResultVo了那就不需要封装了当不等与ResultVo时才进行调用beforeBodyWrite方法跟过滤器的效果是一样的最后重写我们的封装方法beforeBodyWrite注意除了String的返回值有点特殊无法直接封装成json我们需要进行特殊处理其他的直接new ResultVo(data);就ok了打完收工康康效果PostMapping(/findByVo)
public ProductInfo findByVo(Validated ProductInfoVo vo) {ProductInfo productInfo new ProductInfo();BeanUtils.copyProperties(vo, productInfo);return productInfoService.getOne(new QueryWrapper(productInfo));
}此时就算我们返回的是po接收到的返回就是标准格式了开发小哥露出了欣慰的笑容{code: 1000,msg: 请求成功,data: {productId: 1,productName: 泡脚,productPrice: 100.00,productDescription: 中药泡脚加按摩,productStatus: 0,...}
}目前很多公司的项目一般都是要去包装给人感觉很机械应该去灵活处理2. NOT统一响应不开启统一响应原因开发小哥是开心了可是其他系统就不开心了。举个例子我们项目中集成了一个健康检测的功能也就是这货RestController
public class HealthController {GetMapping(/health)public String health() {return success;}
}没办法人家是老大人家要的返回不是{code: 1000,msg: 请求成功,data: success
}人家要的返回只要一个success人家定的标准不可能因为你一个系统改。俗话说的好如果你改变不了环境那你就只能我****新增不进行封装注解因为百分之99的请求还是需要包装的只有个别不需要写在包装的过滤器吧又不是很好维护那就加个注解好了。所有不需要包装的就加上这个注解。Target({ElementType.METHOD})
Retention(RetentionPolicy.RUNTIME)
public interface NotControllerResponseAdvice {
}然后在我们的增强过滤方法上过滤包含这个注解的方法RestControllerAdvice(basePackages {com.bugpool.leilema})
public class ControllerResponseAdvice implements ResponseBodyAdviceObject {Overridepublic boolean supports(MethodParameter methodParameter, Class? extends HttpMessageConverter? aClass) {// response是ResultVo类型或者注释了NotControllerResponseAdvice都不进行包装return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class)|| methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class));}...最后就在不需要包装的方法上加上注解RestController
public class HealthController {GetMapping(/health)NotControllerResponseAdvicepublic String health() {return success;}
}这时候就不会自动封装了而其他没加注解的则依旧自动包装它通过切面编程注释的方式来同一处理一些个别需求是一个处理问题的逻辑五、统一异常 每个系统都会有自己的业务异常比如库存不能小于0子类的这种异常并非程序异常而是业务操作引发的异常我们也需要进行规范的编排业务异常状态码并且写一个专门处理的异常类最后通过刚刚学习过的异常拦截统一进行处理以及打日志1.异常状态码枚举既然是状态码那就肯定要实现我们的标准接口StatusCodeGetter
public enum AppCode implements StatusCode {APP_ERROR(2000, 业务异常),PRICE_ERROR(2001, 价格异常);private int code;private String msg;AppCode(int code, String msg) {this.code code;this.msg msg;}
}2.异常类这里需要强调一下code代表AppCode的异常状态码也就是2000msg代表业务异常这只是一个大类一般前端会放到弹窗title上最后super(message);这才是抛出的详细信息在前端显示在弹窗体中在ResultVo则保存在data中Getter
public class APIException extends RuntimeException {private int code;private String msg;// 手动设置异常public APIException(StatusCode statusCode, String message) {// message用于用户设置抛出错误详情例如当前价格-5小于0super(message);// 状态码this.code statusCode.getCode();// 状态码配套的msgthis.msg statusCode.getMsg();}// 默认异常使用APP_ERROR状态码public APIException(String message) {super(message);this.code AppCode.APP_ERROR.getCode();this.msg AppCode.APP_ERROR.getMsg();}}3.最后进行统一异常的拦截这样无论在service层还是controller层开发人员只管抛出API异常不需要关系怎么返回给前端更不需要关心日志的打印RestControllerAdvice
public class ControllerExceptionAdvice {ExceptionHandler({BindException.class})public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {// 从异常对象中拿到ObjectError对象ObjectError objectError e.getBindingResult().getAllErrors().get(0);return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());}ExceptionHandler(APIException.class)public ResultVo APIExceptionHandler(APIException e) {// log.error(e.getMessage(), e); 由于还没集成日志框架暂且放着写上TODOreturn new ResultVo(e.getCode(), e.getMsg(), e.getMessage());}
}4.最后使用我们的代码只需要这么写if (null orderMaster) {throw new APIException(AppCode.ORDER_NOT_EXIST, 订单号不存在 orderId);
}
{code: 2003,msg: 订单不存在,data: 订单号不存在1998
} 就会自动抛出AppCode.ORDER_NOT_EXIST状态码的响应并且带上异常详细信息订单号不存在xxxx。后端小哥开发有效率前端妹妹获取到2003状态码调用对应警告弹窗title写上订单不存在body详细信息记载订单号不存在1998。同时日志还自动打上去了当然为了避免data 的二意性可以只让前端展示msg即可data可以在排查问题时查看。ps一般对于controller 的处理一般公司都会封装成脚手架拿来直接用即可但是构建原理都大同小异