更新时间:2022-10-03 19:10:14
有时候,我们需要在某个接口中,远程调用第三方的某个接口。
比如:在注册企业时,需要调用天眼查接口,查一下该企业的名称和统一社会信用代码是否正确。
这时候在企业注册接口中,不得不先调用天眼查接口校验数据。如果校验失败,则直接返回。如果校验成功,才允许注册。
如果只是一个企业还好,但如果某个请求有10个企业需要注册,是不是要在企业注册接口中,循环调用10次天眼查接口才能判断所有企业是否正常呢?
public void register(List<Corp> corpList) { for(Corp corp: corpList) { CorpInfo info = tianyanchaService.query(corp); if(null == info) { throw new RuntimeException("企业名称或统一社会信用代码不正确"); } } doRegister(corpList); }
这样做可以,但会导致整个企业注册接口性能很差,极容易出现接口超时问题。
那么,如何解决这类在循环中调用远程接口的问题呢?
远程接口支持批量操作,比如天眼查支持一次性查询多个企业的数据,这样就无需在循环中查询该接口了。
但实际场景中,有些第三方不愿意提供第三方接口。
java8以后通过CompleteFuture
类,实现多个线程查天眼查接口,并且把查询结果统一汇总到一起。
通常情况下,为了在程序中抛出异常时,任然能够继续运行,不至于中断整个程序,我们可以选择手动捕获异常
。例如:
public void run() { try { doSameThing(); } catch (Exception e) { //ignore } doOtherThing(); }
这段代码可以手动捕获异常,保证即使doSameThing方法出现了异常,run方法也能继续执行完。
但有些场景下,手动捕获异常被滥用了。
不知道你在打印异常日志时,有没有写过类似这样的代码:
public void run() throws Exception { try { doSameThing(); } catch (Exception e) { log.error(e.getMessage(), e); throw e; } doOtherThing(); }
通过try/catch关键字,手动捕获异常的目的,仅仅是为了记录错误日志,在接下来的代码中,还是会把该异常抛出。
在每个抛出异常的地方,都捕获一下异常,打印日志。
在写controller层接口方法时,为了保证接口有统一的返回值,你有没有写过类似这样的代码:
@PostMapping("/query") public List<User> query(@RequestBody List<Long> ids) { try { List<User> userList = userService.query(ids); return Result.ok(userList); } catch (Exception e) { log.error(e.getMessage(), e); return Result.fature(500, "服务器内部错误"); } }
在每个controller层的接口方法中,都加上了上面这种捕获异常的逻辑。
上述两种场景中,频繁的捕获异常,会让代码性能降低,因为捕获异常是会消耗性能的。
此外,这么多重复的捕获异常代码,看得让人头疼。
其实,我们还有更好的选择。在网关层(比如:zuul或gateway),有个统一的异常处理代码,既可以打印异常日志,也能统一封装接口返回值,这样可以减少很多异常被滥用的情况。
在我们写代码的时候,打印日志是必不可少的工作之一。
因为日志可以帮我们快速定位问题,判断代码当时真正的执行逻辑。
但打印日志的时候也需要注意,不是说任何时候都要打印日志,比如:
@PostMapping("/query") public List<User> query(@RequestBody List<Long> ids) { log.info("request params:{}", ids); List<User> userList = userService.query(ids); log.info("response:{}", userList); return userList; }
对于有些查询接口,在日志中打印出了请求参数和接口返回值。
咋一看没啥问题。
但如果ids中传入值非常多,比如有1000个。而该接口被调用的频次又很高,一下子就会打印大量的日志,用不了多久就可能把磁盘空间打满。
如果真的想打印这些日志该怎么办?
@PostMapping("/query") public List<User> query(@RequestBody List<Long> ids) { if (log.isDebugEnabled()) { log.debug("request params:{}", ids); } List<User> userList = userService.query(ids); if (log.isDebugEnabled()) { log.debug("response:{}", userList); } return userList; }
使用isDebugEnabled
判断一下,如果当前的日志级别是debug才打印日志。生产环境默认日志级别是info,在有些紧急情况下,把某个接口或者方法的日志级别改成debug,打印完我们需要的日志后,又调整回去。
方便我们定位问题,又不会产生大量的垃圾日志,一举两得。
参数校验是接口必不可少的功能之一,一般情况下,提供给第三方调用的接口,需要做严格的参数校验。
以前我们是这样校验参数的:
@PostMapping("/add") public void add(@RequestBody User user) { if(StringUtils.isEmpty(user.getName())) { throw new RuntimeException("name不能为空"); } if(null != user.getAge()) { throw new RuntimeException("age不能为空"); } if(StringUtils.isEmpty(user.getAddress())) { throw new RuntimeException("address不能为空"); } userService.add(user); }
需要手动写校验的代码,如果作为入参的实体中字段非常多,光是写校验的代码,都需要花费大量的时间。而且这些校验代码,很多都是重复的,会让人觉得恶心。
好消息是使用了hibernate
的参数校验框架validate
之后,参数校验一下子变得简单多了。
我们只需要校验的实体类User中使用validation框架的相关注解,比如:@NotEmpty、@NotNull等,定义需要校验的字段即可。
@NoArgsConstructor @AllArgsConstructor @Data public class User { private Long id; @NotEmpty private String name; @NotNull private Integer age; @NotEmpty private String address; }
然后在controller类上加上@Validated
注解,在接口方法上加上@Valid
注解。
@Slf4j @Validated @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/add") public void add(@RequestBody @Valid User user) { userService.add(user); } }
这样就能自动实现参数校验的功能。
然而,现在需求改了,需要在User类上增加了一个参数Role,它也是必填字段,并且它的roleName和tag字段都不能为空。
但如果我们在校验参数时,不小心把代码写成这样:
@NoArgsConstructor @AllArgsConstructor @Data public class User { private Long id; @NotEmpty private String name; @NotNull private Integer age; @NotEmpty private String address; @NotNull private Role role; }
@Data @NoArgsConstructor @AllArgsConstructor public class Role { @NotEmpty private String roleName; @NotEmpty private String tag; }
结果就悲剧了。
你心里可能还乐呵呵的认为写的代码不错,但实际情况是,roleName和tag字段根本不会被校验到。
如果传入参数:
{ "name": "tom", "age":1, "address":"123", "role":{} }
即使role字段传入的是空对象,但该接口也会返回成功。
那么如何解决这个问题呢?
@NoArgsConstructor @AllArgsConstructor @Data public class User { private Long id; @NotEmpty private String name; @NotNull private Integer age; @NotEmpty private String address; @NotNull @Valid private Role role; }
需要在Role字段上也加上@Valid
注解。
温馨的提醒一声,使用validate框架校验参数一定要自测,因为很容易踩坑。
我之前对接某个第三方时,他们有部分接口的返回值结构是这样的:
{ "ret":0, "message":null, "data":[] }
另一部分接口的返回值结构是这样的:
{ "code":0, "msg":null, "success":true, "result":[] }
整得我有点懵逼。
为啥没有一个统一的返回值?
我需要给他们的接口写两套返回值解析的代码,后面其他人看到了这些代码,可能也会心生疑问,为什么有两种不同的返回值解析?
唯一的解释是一些接口是新项目的,另外一些接口是老项目的。
但如果不管是新项目,还是老项目,如果都有一个统一的对外网关服务,由这个服务进行鉴权和统一封装返回值。
{ "code":0, "message":null, "data":[] }
就不会有返回值结构不一致的问题。
温馨的提醒一下,业务服务不要捕获异常,直接把异常抛给网关服务,由它来统一全局捕获异常,这样就能统一异常的返回值结构。
我们写完代码之后,把代码提交到gitlab
上,也有一些讲究。
最最忌讳的是代码还没有写完,因为赶时间(着急下班),就用git
把代码提交了。例如:
public void test() { String userName="苏三"; String password= }
这段代码中的password变量都没有定义好,项目一运行起来必定报错。
这种错误的代码提交方式,一般是新手会犯。但还有另一种情况,就是在多个分支merge代码的时候,有时候会出问题,merge之后的代码不能正常运行,就被提交了。
好的习惯是:用git提交代码之前,一定要在本地运行一下,确保项目能正常启动才能提交。
宁可不提交代码到远程仓库,切勿因为一时赶时间,提交了不完整的代码,导致团队的队友们项目都启动不了。
有些时候,我们为了偷懒,对有些没用的代码不做任何处理。
比如:
@Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; public void add(User user) { System.out.println("add"); } public void update(User user) { System.out.println("update"); } public void query(User user) { System.out.println("query"); } }
本来UserService类中的add、update、query方法都在用的。后来,某些功能砍掉了,现在只有add方法真正在用。
某一天,项目组来了一个新人,接到需求需要在user表加一个字段,这时候他是不是要把add、update、query方法都仔细看一遍,评估一下影响范围?
后来发现只有add方法需要改,他心想前面的开发者为什么不把没用的代码删掉,或者标记出来呢?
在java中可以使用@Deprecated
表示这个类或者方法没在使用了,例如:
@Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; public void add(User user) { System.out.println("add"); } @Deprecated public void update(User user) { System.out.println("update"); } @Deprecated public void query(User user) { System.out.println("query"); } }
我们在阅读代码时,可以先忽略标记了@Deprecated
注解的方法。这样一个看似简单的举手之劳,可以给自己,或者接手该代码的人,节省很多重复查代码的时间。
建议我们把没用的代码优先删除掉,因为gitlab中是有历史记录的,可以找回。但如果有些为了兼容调用方老版本的代码,不能删除的情况,建议使用
@Deprecated
注解相关类或者接口。
不知道你有没有遇到过这种场景:你写了一个接口,本来以为没人使用,后来觉得接口名或参数名不对,偷偷把它们改了。比如:
@PostMapping("/query") public List<User> query(@RequestBody List<Long> ids) { return userService.query(ids); }
接口名改了:
@PostMapping("/queryUser") public List<User> queryUser(@RequestBody List<Long> ids) { return userService.query(ids); }
结果导致其他人的功能报错,原来他已经在调用该接口了。
大意了。。。
所以在修改接口名、参数名、修改参数类型、修改参数个数时,一定要先询问一下相关同事,有没有使用该接口,免得以后出现不必要的麻烦。
对于已经在线上使用的接口,尽量不要修改接口名、参数名、修改参数类型、修改参数个数,还有请求方式,比如:get改成post等。宁可新加一个接口,也尽量不要影响线上功能。
我之前见过有些小伙伴,在代码中使用map接收参数的。例如:
@PostMapping("/map") public void map(@RequestBody Map<String, Object> mapParam){ System.out.println(mapParam); }
在map方法中使用mapParam对象接收参数,这种做法确实很方便,可以接收多种json格式的数据。
例如:
{ "id":123, "name":"苏三", "age":18, "address":"成都" }
或者:
{ "id":123, "name":"苏三", "age":18, "address":"成都", "role": { "roleName":"角色", "tag":"t1" } }
这段代码可以毫不费劲的接收这两种格式的参数,so cool。
但同时也带来了一个问题,那就是:参数的数据结构你没法控制,有可能你知道调用者传的json数据格式是第一种,还是第二种。但如果你没有写好注释,其他的同事看到这段代码,可能会一脸懵逼,map接收的参数到底是什么东东?
项目后期,这样的代码变得非常不好维护。有些同学接手前人的代码,时不时吐槽一下,是有原因的。
那么,如果优化这种代码呢?
我们应该使用有明确含义的对象去接收参数,例如:
@PostMapping("/add") public void add(@RequestBody @Valid User user){ System.out.println(user); }
其中的User对象是我们已经定义好的对象,就不会存在什么歧义了。
因为项目时间实在太紧了,系统功能都开发不完,更何况是单元测试呢?
大部分人不写单元测试的原因,可能也是这个吧。
但我想告诉你的是,不写单元测试并不是个好习惯。
我见过有些编程高手是测试驱动开发
,他们会先把单元测试写好,再写具体的业务逻辑。
那么,我们为什么要写单元测试呢?
建议由于项目时间非常紧张,在开发时确实没有写单元测试,但在项目后期的空闲时间也建议补上。
本文结合自己的实际工作经验,用调侃的方式,介绍了在编写代码的过程中,不太好的地方和一些优化技巧,给用需要的朋友们一个参考。