前言 因为前段时间新项目已经完成目前趋于稳定,所以最近我被分配到了公司的运维组,负责维护另外一个项目,包含处理客户反馈的日常问题,以及对系统缺陷进行优化。 经过了接近两周的维护,除了日常问题以外,代码层面我一共处理了一个BUG,优化了三个问题,我把这四个问题归纳成了四段编码小技巧分享给大家,希望能有所帮助,今后若遇到类似的问题可以到我这里翻出来看看,想必能节省许多时间。 技巧1、stream分组 很多人都知道java8的stream很好用,但很多人其实不会用,或者说搜了许多资料还是用不好,归根究底就是许多百度的资料没有合适的案例,让人似懂非懂。我这里就从线上项目中提取出了一段stream分组的代码片段,帮大家一看就懂。 首先,我把表结构展示一下,当然为了做案例简化了,方便理解。医生信息表 id doctorname phone photourl areacode 1hr张三 13612345678hrhttps:head。img。comabc。png EAST 2hr李四 15845678901hrhttps:head。img。comxyz。png WEST院区表 id areacode areaname 1hrEAST 东院区 2hrSOUTH 南院区 3hrWEST 西院区 4hrNORTH 北院区 需求:查询医生信息列表,要展示院区名称。 在我做优化之前,上一位同事是这么写的:查询医生列表ListDoctorVOdoctorVOListdoctorService。findDoctorList();遍历医生列表,装入院区名称。doctorVOList。forEach((vo){院区编码StringareaCodevo。getAreaCode();根据院区编码查询院区信息HospitalAreaDTOhospitalAreaDTOareaService。findOneByAreaCode(areaCode);放入院区名称vo。setAreaName(hospitalAreaDTO。getAreaName());});返回returndoctorVOList; 可以看到,他是遍历医生列表,然后分别去查询每个医生所在院区的名称并返回,等于说若有100个医生,那么就要查询100次院区表,虽然MySQL8。0以后的查询效率其实变高了,这种小表查询其实影响没那么大,但作为一个成熟的线上项目,这种代码就是新手水平,我敢打包票很多人都这么写过。 优化后:查询医生列表ListDoctorVOdoctorVOListdoctorService。findDoctorList();以areaCode为key将院区列表分组放入内存中MapString,ListHospitalAreaDTOareaMapareaService。findAll()。stream()。collect(Collectors。groupingBy(ee。getAreaCode()));遍历医生列表,装入院区名称。ListDoctorVOdoctorVOListnewArrayList();doctorVOList。forEach((vo){院区编码StringareaCodevo。getAreaCode();根据院区编码从map中拿到院区名称StringareaNameareaMap。get(areaCode)。get(0)。getAreaName();放入院区名称vo。setAreaName(areaName);});返回returndoctorVOList; 可以看到,这里直接使用stream分组将院区信息按照院区编码为key,院区信息为value放入内存中,然后遍历医生列表时,根据院区编码直接从内存中取到对应的院区名称即可,前后只查询了1次,极大提高了效率,节省了数据库资源。 只要是类似这种遍历查询需要从其他小表查出某属性值的场景时,都可以使用这种方式。 2、stream排序 这个排序其实很简单,就是根据客户要求的多个规则给医生列表排序,这里的规则是:按照是否在线、是否排班降序,且按照医生职称、医生编号升序。 项目中用到了mybatis,所以之前的写法是直接写sql语句,但sql语句复杂一点的话后期交给其他同事是不好维护的。 其实,查出列表后,直接在内存中通过stream进行排序就很舒适,所以我把项目中这部分的sql语句写法优化成了直接在代码中进行查询并排序。 stream多属性不同规则排序:查询列表ListHomePageDoctorsDTOrespDTOListfindHomePageDoctorList();排序ListHomePageDoctorsDTOsortListrespDTOList。stream()。sorted(Comparator。comparing(HomePageDoctorsDTO::getOnlineFlag,Comparator。reverseOrder())。thenComparing(HomePageDoctorsDTO::getScheduleStatus,Comparator。reverseOrder())。thenComparing(HomePageDoctorsDTO::getDoctorTitleSort)。thenComparing(HomePageDoctorsDTO::getDoctorNo))。collect(Collectors。toList());返回returnsortList; 上面一段代码就OK了,十分简单,reverseOrder()表示降序,不写就表示默认的升序。 这里需要注意一点,网上很多资料都有用到: Comparator。comparing(HomePageDoctorsRespDTO::getOnlineFlag)。reverse() 这样的方式来进行降序,这是有误区的,可以专门查下或试下reverse()的用法,它只是反转不是降序排列,类似于从左到右变为从右到左这样的形式,降序一定要用上面代码的写法,这是一个要注意的坑。 3、异步线程 异步线程很多人都知道,直接使用Async注解即可,但很多人不知道使用这个注解的限制条件,往往以为自己用上了,实际上根本没有走异步线程。Async注解只能标注在void方法上;Async注解标注的方法必须是public修饰;Async注解标注的方法和调用方在同一个类中,不会生效。 以上条件缺一不可,哪怕满足前两个也不行,还是不会走异步线程。 我维护的这个项目就是满足了前两个,实际上没有生效,说明写这段代码的同事想法是好的,希望不占用主线程从而提高接口效率,但实际上自己也没有充分测试,以为是有效的,我相信很多人也这么干过。 这里,我优化了下,给大家一个最科学的写法,保证有效,这里我以发短信通知为例。 首先,定义一个专门写异步方法的类叫AsyncService。异步方法的服务,不影响主程序运行。ServicepublicclassAsyncService{privatefinalLoggerlogLoggerFactory。getLogger(AsyncService。class);AutowiredprivatePhoneServicephoneService;发短信通知患者检查时间paramdto患者信息paramconsult咨询信息AsyncpublicvoidsendMsgToPatient(PatientDTOpatientDTO,ConsultDTOconsultDTO){消息内容StringphonepatientDTO。getTelphone();Stringmsg您好,patientDTO。getName(),已成功为你预约consultDTO。getDeviceType()检查,时间是consultDTO。getCheckDate(),望您做好检查时间安排。就诊卡号:consultDTO。getPatientId(),检查项目:consultDTO。getTermName();发短信phoneService。sendPhoneMsg(phone,msg);}} 这里注意,使用public修饰符,void方法,前面限制条件已经讲过。 其次,我们要在配置类中声明EnableAsync注解开启异步线程。ConfigurationEnableAsyncpublicclassAsyncConfigurationimplementsAsyncConfigurer{具体实现。。。。} 最后,我们在业务方法中调用即可。publicBusinessResultdoBusiness(PatientDTOpatientDTO,ConsultDTOconsultDTO){处理业务逻辑,此处省略。。。。。。。异步发短信通知患者检查时间asyncService。sendMsgToPatient(patientDTO,consultDTO);} 这样,这个发短信的业务就会走异步线程,哪怕有其他类似业务需要异步调用,也都可以放到AsyncService中去统一处理。 我们还要注意一点,以上方式的异步线程实际上走的是默认线程池,而默认线程池并不是推荐的,因为在大量使用过程中可能出现线程数不够导致堵塞的情况,所以我们还要进一步优化,使用自定义线程池。 这里,我们使用阿里开发手册中推荐的ThreadPoolTaskExecutor。ConfigurationEnableAsyncpublicclassAsyncConfigurationimplementsAsyncConfigurer{privatefinalLoggerlogLoggerFactory。getLogger(AsyncConfiguration。class);OverrideBean(nametaskExecutor)publicExecutorgetAsyncExecutor(){log。debug(CreatingAsyncTaskExecutor);ThreadPoolTaskExecutorexecutornewThreadPoolTaskExecutor();executor。setCorePoolSize(8);executor。setMaxPoolSize(50);executor。setQueueCapacity(1000);executor。setThreadNamePrefix(asyncExecutor);returnexecutor;}OverridepublicAsyncUncaughtExceptionHandlergetAsyncUncaughtExceptionHandler(){returnnewSimpleAsyncUncaughtExceptionHandler();}} 这里,我们分别设置了核心线程数8、最大线程数50、任务队列1000,线程名称以asyncExecutor开头。 这些配置其实可以提取出来放到yml文件中,具体配置多少要结合项目使用异步线程的规模以及服务器自身的水平来判断,我们这个项目用到异步线程的地方不算太多,主要是发短信通知和订阅消息通知时,而且服务器本身是8核16G,所以这个设置是相对符合的。 4、统一异常管理 统一异常管理是我着重要讲的,这次我维护的项目中在这块写的简直是难以忍受,线上排查问题很多重要的信息啥也看不到,检查代码发现明明用到了统一异常管理,但写法简直是外行水准,气的我肚子疼。 首先,我说一下规范:统一异常管理后,如非必要绝不能再try。。。catch,如果必须try。。。catch请一定要log。error(e)记录日志打印堆栈信息,并且throw异常,否则该代码块出问题线上什么也看不到; 统一异常管理后,接口层面校验错误时不要直接使用通用响应对象返回,比如ResultUtil。error(500,查询xx失败),这样会导致统一异常管理失去效能,因为这就是正常返回了一个对象,不是出现异常,所以我们应该在校验错误时直接thrownewBusinessException(查询xx失败)主动抛出一个异常,这样才会被捕获到; 统一异常管理后,全局异常管理类中最好使用Spring自带的ResponseEntity包装一层,保证异常时HTTP状态不是200,而是正确的异常状态,这样前端工程师才能根据HTTP状态判断接口连通性,然后再根据业务状态判断接口获取数据是否成功。 这里,我把项目中优化后的全局异常统一处理代码贴上来分享给大家: 首先,我们自定义三个常用异常。 校验参数的异常,继承运行时异常RuntimeException。参数不正确异常publicclassBadArgumentExceptionextendsRuntimeException{publicBadArgumentException(){super();}publicBadArgumentException(StringerrMsg){super(errMsg);}} 校验权限的异常,继承运行时异常RuntimeException。无访问权限异常publicclassNotAuthorityExceptionextendsRuntimeException{publicNotAuthorityException(){super(没有访问权限。);}publicNotAuthorityException(StringerrMsg){super(errMsg);}} 业务逻辑异常,继承运行时异常RuntimeException。业务逻辑异常publicclassBusinessExceptionextendsRuntimeException{publicBusinessException(){super();}publicBusinessException(StringerrMsg){super(errMsg);}publicBusinessException(StringerrMsg,Throwablethrowable){super(errMsg,throwable);}} 其次,我们声明一个全局异常处理类。统一异常处理RestControllerAdviceSlf4jpublicclassExceptoinTranslator{权限异常ExceptionHandler(value{AccessDeniedException。class,NotAuthorityException。class})publicResponseEntityhandleNoAuthorities(Exceptionex){returnResponseEntity。status(HttpCodeEnum。FORBIDDEN。getCode())。body(ResultUtil。forbidden(ex。getMessage()));}参数错误异常ExceptionHandler(valueBadArgumentException。class)publicResponseEntityhandleBadArgument(Exceptionex){returnResponseEntity。status(HttpStatus。BADREQUEST。value())。body(ResultUtil。custom(HttpStatus。BADREQUEST。value(),ex。getMessage()));}接口参数校验异常ExceptionHandler(valueMethodArgumentNotValidException。class)publicResponseEntityhandleArguNotValid(MethodArgumentNotValidExceptionex){FieldErrorfieldErrorex。getBindingResult()。getFieldErrors()。get(0);Stringmsg!StringUtils。isEmpty(fieldError。getDefaultMessage())?fieldError。getDefaultMessage():参数不合法;returnResponseEntity。status(HttpStatus。BADREQUEST。value())。body(ResultUtil。custom(HttpStatus。BADREQUEST。value(),msg));}参数不合法异常ExceptionHandler(valueConstraintViolationException。class)publicResponseEntityhandleConstraintViolation(ConstraintViolationExceptionex){Stringerrex。getMessage();SetConstraintViolationlt;?setex。getConstraintViolations();if(!set。isEmpty()){errset。iterator()。next()。getMessage();}StringmsgStringUtils。isEmpty(err)?参数不合法:err;returnResponseEntity。status(HttpStatus。BADREQUEST。value())。body(ResultUtil。custom(HttpStatus。BADREQUEST。value(),msg));}参数不合法异常ExceptionHandler(value{IllegalArgumentException。class})publicResponseEntityhandleIllegalArgu(Exceptionex){Stringerrex。getMessage();StringmsgStringUtils。isEmpty(err)?参数不合法:err;returnResponseEntity。status(HttpStatus。BADREQUEST。value())。body(ResultUtil。custom(HttpStatus。BADREQUEST。value(),msg));}业务逻辑处理异常,也是我们最常用的主动抛出的异常。ExceptionHandler(valueBusinessException。class)publicResponseEntityhandleBadBusiness(Exceptionex){returnResponseEntity。status(HttpStatus。INTERNALSERVERERROR。value())。body(ResultUtil。custom(HttpStatus。INTERNALSERVERERROR。value(),ex。getMessage()));}HTTP请求方法不支持异常ExceptionHandler(valueHttpRequestMethodNotSupportedException。class)publicResponseEntitymethodNotSupportException(Exceptionex){returnResponseEntity。status(HttpStatus。METHODNOTALLOWED。value())。body(ResultUtil。custom(HttpStatus。METHODNOTALLOWED。value(),请求方法不支持!));}除上面以外所有其他异常的处理会进入这里ExceptionHandler(valueException。class)publicResponseEntityhandleException(Exceptionex){log。error(〔ExceptoinTranslator〕全局异常:,ex);returnResponseEntity。status(HttpStatus。INTERNALSERVERERROR。value())。body(ResultUtil。custom(HttpStatus。INTERNALSERVERERROR。value(),发生内部错误!));}} 上面这个全局异常处理,包含了项目最有可能出现的:几种参数异常、权限异常、HTTP方法不支持异常、自定义业务异常、其他异常,基本上够用了,如果还想更细致一点还可以自定义其他的异常放进来。 这里要关注的两点是: 1、我们统一使用Spring的ResponseEntity进行了外层包装,而不是直接使用自定义响应对象ResultUtil来返回,这样保证了我们接口返回的业务状态和接口本身的HTTP状态是一致的,前端就可以判断接口连通性了,如果不明白区别,使用一下Postman就可以看到右上角的HTTP状态了,你使用自定义响应对象返回时永远都是200; 2、最后其他所有异常Exception。class的捕获,务必进行log。error(ex)日志记录,这样线上排查时才能看到具体的堆栈信息。 总结合理利用stream分组提高查询效率; stream排序避免踩坑; 异步线程最佳用法; 统一异常管理最佳使用方式。 本文作者:福隆苑居士 本文链接:https:www。cnblogs。comfulongyuanjuship16153733。html