1. 前言
统一返回值封装、统一异常处理和异常错误码体系的意义在于提高代码的可维护性和可读性,使得代码更加健壮和稳定。统一返回值封装可以避免每一个接口都需要手工拼装响应报文;统一异常处理可以将异常处理的逻辑集中到一个地方,避免代码中出现大量的try-catch语句,降低了代码的复杂度,提高了代码的可读性;异常体系的设计可以清晰地区分不同类型的异常,使得开发者能够更加精准地处理异常情况,并且能够更好地定位和解决问题。
Graceful Response是一个Spring Boot体系下的优雅响应处理组件,提供一站式统一返回值封装、全局异常处理、自定义异常错误码、自定义参数校验异常码等功能,使用Graceful Response进行web接口开发不仅可以节省大量的时间,还可以提高代码质量,使代码逻辑更清晰。
强烈推荐你花3分钟学会它!
Graceful Response的Github地址: https://github.com/feiniaojin/graceful-response ,欢迎star!
Graceful Response的案例工程代码:https://github.com/feiniaojin/graceful-response-example.git
2. Spring Boot Web API接口数据返回的现状
我们进行Spring Boo Web API接口开发时,通常大部分的Controller代码是这样的:
1public class Controller { 2 @GetMapping("/query") 3 @ResponseBody 4 public Response query(Parameter params) { 5 6 Response res = new Response(); 7 try { 8 //1.校验params参数,非空校验、长度校验 9 if (illegal(params)) { 10 res.setCode(1); 11 res.setMsg("error"); 12 return res; 13 } 14 //2.调用Service的一系列操作 15 Data data = service.query(params); 16 //3.执行正确时,将操作结果设置到res对象中 17 res.setData(data); 18 res.setCode(0); 19 res.setMsg("ok"); 20 return res; 21 } catch (BizException1 e) { 22 //4.异常处理:一堆丑陋的try...catch,如果有错误码的,还需要手工填充错误码 23 res.setCode(1024); 24 res.setMsg("error"); 25 return res; 26 } catch (BizException2 e) { 27 //4.异常处理:一堆丑陋的try...catch,如果有错误码的,还需要手工填充错误码 28 res.setCode(2048); 29 res.setMsg("error"); 30 return res; 31 } catch (Exception e) { 32 //4.异常处理:一堆丑陋的try...catch,如果有错误码的,还需要手工填充错误码 33 res.setCode(1); 34 res.setMsg("error"); 35 return res; 36 } 37 } 38}
这段代码存在什么问题呢?真正的业务逻辑被冗余代码淹没,可读性太差。
真正执行业务的代码只有
Data data=service.query(params);
其他代码不管是正常执行还是异常处理,都是为了异常封装、把结果封装为特定的格式,例如以下格式:
1{ 2 "code": 0, 3 "msg": "ok", 4 "data": { 5 "id": 1, 6 "name": "username" 7 } 8}
这样的逻辑每个接口都需要处理一遍,都是繁琐的重复劳动。
现在,只需要引入Graceful Response组件并通过@EnableGracefulResponse启用,就可以直接返回业务结果并自动完成response的格式封装。
以下是使用Graceful Response之后的代码,实现同样的返回值封装、异常处理、异常错误码功能,但可以看到代码变得非常简洁,可读性非常强。
1public class Controller { 2 @GetMapping("/query") 3 @ResponseBody 4 public Data query(Parameter params) { 5 return service.query(params); 6 } 7}
3. 快速入门
3.1 引入maven依赖
graceful-response已发布至maven中央仓库,可以直接引入到项目中,maven依赖如下:
1<dependency> 2 <groupId>com.feiniaojin</groupId> 3 <artifactId>graceful-response</artifactId> 4 <version>{此处替换为最新的版本号}</version> 5</dependency>
以下链接可以查看maven中央仓库中最新的版本:
https://central.sonatype.com/artifact/com.feiniaojin/graceful-response/3.0/versions
3.2 在启动类中引入@EnableGracefulResponse注解
1@EnableGracefulResponse 2@SpringBootApplication 3public class ExampleApplication { 4 public static void main(String[] args) { 5 SpringApplication.run(ExampleApplication.class, args); 6 } 7}
3.3 Controller方法直接返回结果
•普通的查询
1@Controller 2public class Controller { 3 @RequestMapping("/get") 4 @ResponseBody 5 public UserInfoView get(Long id) { 6 log.info("id={}", id); 7 return UserInfoView.builder().id(id).name("name" + id).build(); 8 } 9}
UserInfoView的源码:
1@Data 2@Builder 3public class UserInfoView { 4 private Long id; 5 private String name; 6}
这个接口直接返回了 UserInfoView的实例对象,调用接口时,Graceful Response将自动封装为以下格式:
1{ 2 "status": { 3 "code": "0", 4 "msg": "ok" 5 }, 6 "payload": { 7 "id": 1, 8 "name": "name1" 9 } 10}
可以看到UserInfoView被自动封装到payload字段中。
Graceful Response提供了两种风格的Response,可以通过在application.properties文件中配置gr.responseStyle=1,将以以下的格式进行返回:
1{ 2 "code": "0", 3 "msg": "ok", 4 "data": { 5 "id": 1, 6 "name": "name1" 7 } 8}
如果这两种风格也不能满足需要,我们还可以根据自己的需要进行自定义返回的Response格式。详细见本文 4.3自定义Respnse格式。
•异常处理的场景
通过Graceful Response,我们不需要专门在Controller中处理异常,详细见 4.1 Graceful Response异常错误码处理。
•返回值为空的场景
某些Command类型的方法只执行修改操作,不返回数据,这个时候我们可以直接在Controller中返回void,Graceful Response会自动封装默认的操作成功Response报文。
1@Controller 2public class Controller { 3 @RequestMapping("/void") 4 @ResponseBody 5 public void testVoidResponse() { 6 //省略业务操作 7 } 8}
testVoidResponse方法的返回时void,调用这个接口时,将返回:
1{ 2 "status": { 3 "code": "200", 4 "msg": "success" 5 }, 6 "payload": {} 7}
3.4 Service方法业务处理
在引入Graceful Response后,Service层的方法的可读性可以得到极大的提升。
•接口直接返回业务数据类型,而不是Response,更具备可读性
1public interface ExampleService { 2 UserInfoView query1(Query query); 3}
•Service接口实现类中,直接抛自定义的业务异常,Graceful Response将其转化为返回错误码和错误提示
1public class ExampleServiceImpl implements ExampleService { 2 @Resource 3 private UserInfoMapper mapper; 4 5 public UserInfoView query1(Query query) { 6 UserInfo userInfo = mapper.findOne(query.getId()); 7 if (Objects.isNull(userInfo)) { 8 //这里直接抛自定义异常,异常通过@ExceptionMapper修饰,提供异常码和异常提示 9 throw new NotFoundException(); 10 } 11 // 省略后续业务操作 12 } 13}
1/** 2 * NotFoundException的定义,使用@ExceptionMapper注解修饰 3 * code:代表接口的异常码 4 * msg:代表接口的异常提示 5 */ 6@ExceptionMapper(code = "1404", msg = "找不到对象") 7public class NotFoundException extends RuntimeException { 8 9}
1//Controller不再捕获处理异常 2@RequestMapping("/get") 3@ResponseBody 4public UserInfoView get(Query query)) { 5 return exampleService.query1(query); 6}
当Service方法抛出NotFoundException异常时,接口将直接返回错误码,不需要手工set,极大地简化了异常处理逻辑。
1{ 2 "status": { 3 "code": "1404", 4 "msg": "找不到对象" 5 }, 6 "payload": {} 7}
验证:启动example工程后,请求
http://localhost:9090/example/notfound
3.5 通用异常类和通用工具类
@ExceptionMapper设计的初衷是将异常与错误码关联起来,用户只需要抛异常,不需要再关注异常与错误码的对应关系。
部分用户反馈,希望在不自定义新异常类的情况下,也能可以按照预期返回错误码和异常信息,因此从2.1版本开始,新增了GracefulResponseException异常类,用户只需要抛出该异常即可。
1public class Service { 2 3 public void method() { 4 throw new GracefulResponseException("自定义的错误码","自定义的错误信息"); 5 } 6}
为简化使用,从2.1版本开始提供GracefulResponse通用工具类,在需要抛出GracefulResponseException时,只需要调用raiseException方法即可。 这样设计原因是用户抛通用异常,其实已经不关心具体是什么异常了,用户实际上只是想要错误码和错误信息。
示例如下:
1public class Service { 2 3 public void method() { 4 5 //当condition==true时,抛出GracefulResponseException异常,返回自定义的错误码和错误信息 6 if (condition) { 7 GracefulResponse.raiseException("自定义的错误码", "自定义的错误信息"); 8 } 9 //省略其他业务逻辑 10 } 11}
3.6 参数校验异常以及错误码
在3.0版本以前,如果引用validation框架并发生了校验异常,Graceful Response在默认情况下会捕获并返回code=1,参数校验发生的异常信息会丢失;如果使用异常别名功能,可以对大的校验异常返回统一的错误码,但是不够灵活且依旧没有解决参数异常提示的问题。
Graceful Response从3.0版本开始,引入@ValidationStatusCode注解,可以非常方便地支持validation校验异常。
@ValidationStatusCode注解目前只有一个code属性,用于指定参数校验异常时的错误码,错误提示则取自validation校验框架。
- 对入参类进行参数校验
1@Data 2public class UserInfoQuery { 3 4 @NotNull(message = "userName is null !") 5 @Length(min = 6, max = 12) 6 @ValidationStatusCode(code = "520") 7 private String userName; 8 9}
当userName字段任意一项校验不通过时,接口将会返回异常码520和校验注解中的message:
1{ 2 "status": { 3 "code": "520", 4 "msg": "userName is null !" 5 }, 6 "payload": {} 7}
详细见example工程ExampleController的validateDto方法
http://localhost:9090/example/validateDto
注意:@ValidationStatusCode校验参数对象字段的情况,code取值顺序为:会先取字段上的注解,再去该属性所在对象的类(即UserInfoQuery类)上的注解,再取全局配置的参数异常码
gr.defaultValidateErrorCode,最后取默认的全局默认的错误码(默认code=1)
- 直接在Controller中校验方法入参
直接在Controller方法中进行参数校验:
1@Validated 2public class ExampleController { 3 4 @RequestMapping("/validateMethodParam") 5 @ResponseBody 6 @ValidationStatusCode(code = "1314") 7 public void validateMethodParam(@NotNull(message = "userId不能为空") Long userId, 8 @NotNull(message = "userName不能为空") Long userName) { 9 //省略业务逻辑 10 } 11}
当userId、或者userName校验不通过时,将会返回code=1314,msg为对应的校验信息。
1{ 2 "status": { 3 "code": "1314", 4 "msg": "userId不能为空" 5 }, 6 "payload": {} 7}
详细见example工程ExampleController的validateMethodParam方法
http://localhost:9090/example/validateMethodParam
注意:@ValidationStatusCode校验Controller方法参数字段的情况,code取值顺序为:会先取当前方法上的注解,再去该方法所在类(即ExampleController类)上的注解,再取全局配置的参数异常码
gr.defaultValidateErrorCode,最后取默认的全局默认的错误码(默认code=1)
4. 进阶用法
4.1 Graceful Response异常错误码处理
以下是使用Graceful Response进行异常、错误码处理的开发步骤。
•创建自定义异常
通过继承RuntimeException类创建自定义的异常,采用 @ExceptionMapper注解修饰,注解的 code属性为返回码,msg属性为错误提示信息。
关于是继承RuntimeException还是继承Exception,读者可以根据实际情况去选择,Graceful Response对两者都支持。
1@ExceptionMapper(code = "1007", msg = "有内鬼,终止交易") 2public static final class RatException extends RuntimeException { 3 4}
•Service执行具体逻辑
Service执行业务逻辑的过程中,需要抛异常的时候直接抛出去即可。由于已经通过@ExceptionMapper定义了该异常的错误码,我们不需要再单独的维护异常码枚举与异常类的关系。
1//Service层伪代码 2public class Service { 3 public void illegalTransaction() { 4 //需要抛异常的时候直接抛 5 if (check()) { 6 throw new RatException(); 7 } 8 doIllegalTransaction(); 9 } 10}
Controller层调用Service层伪代码:
1public class Controller { 2 @RequestMapping("/test3") 3 public void test3() { 4 //Controller中不会进行异常处理,也不会手工set错误码,只关心核心操作,其他的统统交给Graceful Response 5 exampleService.illegalTransaction(); 6 } 7}
在浏览器中请求controller的/test3方法,有异常时将会返回:
1{ 2 "status": { 3 "code": "1007", 4 "msg": "有内鬼,终止交易" 5 }, 6 "payload": { 7 } 8}
4.2 外部异常别名
案例工程( https://github.com/feiniaojin/graceful-response-example.git )启动后, 通过浏览器访问一个不存在的接口,例如 http://localhost:9090/example/get2?id=1
如果没开启Graceful Response,将会跳转到404页面,主要原因是应用内部产生了 NoHandlerFoundException异常。如果开启了Graceful Response,默认会返回code=1的错误码。
这类非自定义的异常,如果需要自定义一个错误码返回,将不得不对每个异常编写Advice逻辑,在Advice中设置错误码和提示信息,这样做也非常繁琐。
Graceful Response可以非常轻松地解决给这类外部异常定义错误码和提示信息的问题。
以下为操作步骤:
•创建异常别名,并用 @ExceptionAliasFor注解修饰
1@ExceptionAliasFor(code = "1404", msg = "Not Found", aliasFor = NoHandlerFoundException.class) 2public class NotFoundException extends RuntimeException { 3}
code:捕获异常时返回的错误码
msg:异常提示信息
aliasFor:表示将成为哪个异常的别名,通过这个属性关联到对应异常。
•注册异常别名
创建一个继承了AbstractExceptionAliasRegisterConfig的配置类,在实现的registerAlias方法中进行注册。
1@Configuration 2public class GracefulResponseConfig extends AbstractExceptionAliasRegisterConfig { 3 4 @Override 5 protected void registerAlias(ExceptionAliasRegister aliasRegister) { 6 aliasRegister.doRegisterExceptionAlias(NotFoundException.class); 7 } 8}
•浏览器访问不存在的URL
再次访问 http://localhost:9090/example/get2?id=1 ,服务端将返回以下json,正是在ExceptionAliasFor中定义的内容
1{ 2 "code": "1404", 3 "msg": "not found", 4 "data": { 5 } 6}
4.3 自定义Response格式
Graceful Response内置了两种风格的响应格式,可以在application.properties文件中通过gr.responseStyle进行配置。
•gr.responseStyle=0,或者不配置(默认情况)
将以以下的格式进行返回:
1{ 2 "status": { 3 "code": "1007", 4 "msg": "有内鬼,终止交易" 5 }, 6 "payload": { 7 } 8}
•gr.responseStyle=1
将以以下的格式进行返回:
1{ 2 "code": "1404", 3 "msg": "not found", 4 "data": { 5 } 6}
•自定义响应格式
如果以上两种格式均不能满足业务需要,可以通过自定义去满足,Response
例如以下响应:
1public class CustomResponseImpl implements Response { 2 3 private String code; 4 5 private Long timestamp = System.currentTimeMillis(); 6 7 private String msg; 8 9 private Object data = Collections.EMPTY_MAP; 10 11 @Override 12 public void setStatus(ResponseStatus statusLine) { 13 this.code = statusLine.getCode(); 14 this.msg = statusLine.getMsg(); 15 } 16 17 @Override 18 @JsonIgnore 19 public ResponseStatus getStatus() { 20 return null; 21 } 22 23 @Override 24 public void setPayload(Object payload) { 25 this.data = payload; 26 } 27 28 @Override 29 @JsonIgnore 30 public Object getPayload() { 31 return null; 32 } 33 34 public String getCode() { 35 return code; 36 } 37 38 public void setCode(String code) { 39 this.code = code; 40 } 41 42 public String getMsg() { 43 return msg; 44 } 45 46 public void setMsg(String msg) { 47 this.msg = msg; 48 } 49 50 public Object getData() { 51 return data; 52 } 53 54 public void setData(Object data) { 55 this.data = data; 56 } 57 58 public Long getTimestamp() { 59 return timestamp; 60 } 61}
注意,不需要返回的属性可以返回null或者加上@JsonIgnore注解
•配置gr.responseClassFullName
将CustomResponseImpl的全限定名配置到gr.responseClassFullName属性。
gr.responseClassFullName=com.feiniaojin.gracefuresponse.example.config.CustomResponseImpl
注意,配置gr.responseClassFullName后,gr.responseStyle将不再生效。
实际的响应报文如下:
1{ 2 "code":"200", 3 "timestamp":1682489591319, 4 "msg":"success", 5 "data":{ 6 7 } 8}
如果还是不能满足需求,那么可以考虑同时自定义实现Response和ResponseFactory这两个接口。
5. 常用配置
Graceful Response在版本迭代中,根据用户反馈提供了一些常用的配置项,列举如下:
- gr.printExceptionInGlobalAdvice
是否打印异常日志,默认为false - gr.responseClassFullName
自定义Response类的全限定名,默认为空。 配置gr.responseClassFullName后,gr.responseStyle将不再生效 - gr.responseStyle
Response风格,不配置默认为0 - gr.defaultSuccessCode
自定义的成功响应码,不配置则为0 - gr.defaultSuccessMsg
自定义的成功提示,默认为ok - gr.defaultErrorCode
自定义的失败响应码,默认为1 - gr.defaultErrorMsg
自定义的失败提示,默认为error - gr.defaultValidateErrorCode
全局的参数校验错误码,默认等于gr.defaultErrorCode
6. 总结
本文介绍了Graceful Response这个框架的使用,读者在使用过程中遇到问题,欢迎到GitHub提交issue进行反馈,帮助我们将Graceful Response优化得更好。
