前言

因为前段时间新项目已经完成目前趋于稳定,所以最近我被分配到了公司的运维组,负责维护另外一个项目,包含处理客户反馈的日常问题,以及对系统缺陷进行优化。

经过了接近两周的维护,除了日常问题以外,代码层面我一共处理了一个BUG,优化了三个问题,我把这四个问题归纳成了四段编码小技巧分享给大家,希望能有所帮助,今后若遇到类似的问题可以到我这里翻出来看看,想必能节省许多时间。

技巧

1、stream分组

很多人都知道java8的stream很好用,但很多人其实不会用,或者说搜了许多资料还是用不好,归根究底就是许多百度的资料没有合适的案例,让人似懂非懂。我这里就从线上项目中提取出了一段stream分组的代码片段,帮大家一看就懂。

首先,我把表结构展示一下,当然为了做案例简化了,方便理解。

  • 医生信息表
id doctor_name phone photo_url area_code
1 张三 13612345678 https://head.img.com/abc.png EAST
2 李四 15845678901 https://head.img.com/xyz.png WEST
  • 院区表
id area_code area_name
1 EAST 东院区
2 SOUTH 南院区
3 WEST 西院区
4 NORTH 北院区

需求:查询医生信息列表,要展示院区名称。

在我做优化之前,上一位同事是这么写的:

// 查询医生列表
List<DoctorVO> doctorVOList = doctorService.findDoctorList(); // 遍历医生列表,装入院区名称。
doctorVOList.forEach((vo)->{
// 院区编码
String areaCode = vo.getAreaCode();
// 根据院区编码查询院区信息
HospitalAreaDTO hospitalAreaDTO = areaService.findOneByAreaCode(areaCode);
// 放入院区名称
vo.setAreaName(hospitalAreaDTO.getAreaName());
}); // 返回
return doctorVOList;

可以看到,他是遍历医生列表,然后分别去查询每个医生所在院区的名称并返回,等于说若有100个医生,那么就要查询100次院区表,虽然MySQL8.0+以后的查询效率其实变高了,这种小表查询其实影响没那么大,但作为一个成熟的线上项目,这种代码就是新手水平,我敢打包票很多人都这么写过。

优化后:

// 查询医生列表
List<DoctorVO> doctorVOList = doctorService.findDoctorList(); // 以areaCode为key将院区列表分组放入内存中
Map<String,List<HospitalAreaDTO>> areaMap = areaService.findAll().stream()
.collect(Collectors.groupingBy(e-> e.getAreaCode())); // 遍历医生列表,装入院区名称。
List<DoctorVO> doctorVOList = new ArrayList<>();
doctorVOList.forEach((vo)->{
// 院区编码
String areaCode = vo.getAreaCode();
// 根据院区编码从map中拿到院区名称
String areaName = areaMap.get(areaCode).get(0).getAreaName();
// 放入院区名称
vo.setAreaName(areaName);
}); // 返回
return doctorVOList;

可以看到,这里直接使用stream分组将院区信息按照院区编码为key,院区信息为value放入内存中,然后遍历医生列表时,根据院区编码直接从内存中取到对应的院区名称即可,前后只查询了1次,极大提高了效率,节省了数据库资源。

只要是类似这种遍历查询需要从其他小表查出某属性值的场景时,都可以使用这种方式。

2、stream排序

这个排序其实很简单,就是根据客户要求的多个规则给医生列表排序,这里的规则是:按照是否在线、是否排班降序,且按照医生职称、医生编号升序。

项目中用到了mybatis,所以之前的写法是直接写sql语句,但sql语句复杂一点的话后期交给其他同事是不好维护的。

其实,查出列表后,直接在内存中通过stream进行排序就很舒适,所以我把项目中这部分的sql语句写法优化成了直接在代码中进行查询并排序。

stream多属性不同规则排序:

// 查询列表
List<HomePageDoctorsDTO> respDTOList = findHomePageDoctorList(); // 排序
List<HomePageDoctorsDTO> sortList = respDTOList.stream()
.sorted(
Comparator.comparing(HomePageDoctorsDTO::getOnlineFlag, Comparator.reverseOrder())
.thenComparing(HomePageDoctorsDTO::getScheduleStatus, Comparator.reverseOrder())
.thenComparing(HomePageDoctorsDTO::getDoctorTitleSort)
.thenComparing(HomePageDoctorsDTO::getDoctorNo)
)
.collect(Collectors.toList()); // 返回
return sortList;

上面一段代码就OK了,十分简单,reverseOrder()表示降序,不写就表示默认的升序。

这里需要注意一点,网上很多资料都有用到:

Comparator.comparing(HomePageDoctorsRespDTO::getOnlineFlag).reverse()

这样的方式来进行降序,这是有误区的,可以专门查下或试下reverse()的用法,它只是反转不是降序排列,类似于从左到右变为从右到左这样的形式,降序一定要用上面代码的写法,这是一个要注意的坑。

3、异步线程

异步线程很多人都知道,直接使用@Async注解即可,但很多人不知道使用这个注解的限制条件,往往以为自己用上了,实际上根本没有走异步线程。

  1. @Async注解只能标注在void方法上;
  2. @Async注解标注的方法必须是public修饰;
  3. @Async注解标注的方法和调用方在同一个类中,不会生效。

以上条件缺一不可,哪怕满足前两个也不行,还是不会走异步线程。

我维护的这个项目就是满足了前两个,实际上没有生效,说明写这段代码的同事想法是好的,希望不占用主线程从而提高接口效率,但实际上自己也没有充分测试,以为是有效的,我相信很多人也这么干过。

这里,我优化了下,给大家一个最科学的写法,保证有效,这里我以发短信通知为例。

首先,定义一个专门写异步方法的类叫AsyncService。

/**
 * 异步方法的服务, 不影响主程序运行。
 */
@Service
public class AsyncService {
private final Logger log = LoggerFactory.getLogger(AsyncService.class); @Autowired
    private PhoneService phoneService; /**
     * 发短信通知患者检查时间
     * @param dto 患者信息
     * @param consult 咨询信息
     */
    @Async
    public void sendMsgToPatient(PatientDTO patientDTO, ConsultDTO consultDTO) {
// 消息内容
        String phone = patientDTO.getTelphone();
        String msg = "您好,"+ patientDTO.getName() +",已成功为你预约"
+ consultDTO.getDeviceType() +"检查,时间是"+ consultDTO.getCheckDate() 
+",望您做好检查时间安排。就诊卡号:"+ consultDTO.getPatientId() 
+",检查项目:" + consultDTO.getTermName(); // 发短信
        phoneService.sendPhoneMsg(phone, msg);
    }
}

这里注意,使用public修饰符,void方法,前面限制条件已经讲过。

其次,我们要在配置类中声明@EnableAsync注解开启异步线程。

@Configuration
@EnableAsync
public class AsyncConfiguration implements AsyncConfigurer { // 具体实现
// .... }

最后,我们在业务方法中调用即可。

public BusinessResult doBusiness(PatientDTO patientDTO, ConsultDTO consultDTO) {
// 处理业务逻辑,此处省略...
// .... // 异步发短信通知患者检查时间
asyncService.sendMsgToPatient(patientDTO, consultDTO);
}

这样,这个发短信的业务就会走异步线程,哪怕有其他类似业务需要异步调用,也都可以放到AsyncService中去统一处理。

我们还要注意一点,以上方式的异步线程实际上走的是默认线程池,而默认线程池并不是推荐的,因为在大量使用过程中可能出现线程数不够导致堵塞的情况,所以我们还要进一步优化,使用自定义线程池。

这里,我们使用阿里开发手册中推荐的ThreadPoolTaskExecutor。

@Configuration
@EnableAsync
public class AsyncConfiguration implements AsyncConfigurer { private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class); @Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
log.debug("Creating Async Task Executor");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("async-Executor-");
return executor;
} @Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}

这里,我们分别设置了核心线程数8、最大线程数50、任务队列1000,线程名称以async-Executor-开头。

这些配置其实可以提取出来放到yml文件中,具体配置多少要结合项目使用异步线程的规模以及服务器自身的水平来判断,我们这个项目用到异步线程的地方不算太多,主要是发短信通知和订阅消息通知时,而且服务器本身是8核16G,所以这个设置是相对符合的。

4、统一异常管理

统一异常管理是我着重要讲的,这次我维护的项目中在这块写的简直是难以忍受,线上排查问题很多重要的信息啥也看不到,检查代码发现明明用到了统一异常管理,但写法简直是外行水准,气的我肚子疼。

首先,我说一下规范:

  1. 统一异常管理后,如非必要绝不能再try...catch,如果必须try...catch请一定要log.error(e)记录日志打印堆栈信息,并且throw异常,否则该代码块出问题线上什么也看不到;

  2. 统一异常管理后,接口层面校验错误时不要直接使用通用响应对象返回,比如ResultUtil.error(500, "查询xx失败"),这样会导致统一异常管理失去效能,因为这就是正常返回了一个对象,不是出现异常,所以我们应该在校验错误时直接throw new BusinessException("查询xx失败")主动抛出一个异常,这样才会被捕获到;

  3. 统一异常管理后,全局异常管理类中最好使用Spring自带的ResponseEntity包装一层,保证异常时HTTP状态不是200,而是正确的异常状态,这样前端工程师才能根据HTTP状态判断接口连通性,然后再根据业务状态判断接口获取数据是否成功。

这里,我把项目中优化后的全局异常统一处理代码贴上来分享给大家:

首先,我们自定义三个常用异常。

校验参数的异常,继承运行时异常RuntimeException。

/**
* 参数不正确异常
*/
public class BadArgumentException extends RuntimeException {
public BadArgumentException(){
super();
} public BadArgumentException(String errMsg){
super(errMsg);
}
}

校验权限的异常,继承运行时异常RuntimeException。

/**
* 无访问权限异常
*/
public class NotAuthorityException extends RuntimeException { public NotAuthorityException(){
super("没有访问权限。");
} public NotAuthorityException(String errMsg){
super(errMsg);
}
}

业务逻辑异常,继承运行时异常RuntimeException。

/**
* 业务逻辑异常
*/
public class BusinessException extends RuntimeException { public BusinessException(){
super();
} public BusinessException(String errMsg){
super(errMsg);
}
public BusinessException(String errMsg,Throwable throwable){
super(errMsg,throwable);
} }

其次,我们声明一个全局异常处理类。

/**
* 统一异常处理
*/
@RestControllerAdvice
@Slf4j
public class ExceptoinTranslator { /**
* 权限异常
*/
@ExceptionHandler(value = {AccessDeniedException.class,NotAuthorityException.class})
public ResponseEntity handleNoAuthorities(Exception ex){
return ResponseEntity.status(HttpCodeEnum.FORBIDDEN.getCode()).body(
ResultUtil.forbidden(ex.getMessage())
);
} /**
* 参数错误异常
*/
@ExceptionHandler(value = BadArgumentException.class)
public ResponseEntity handleBadArgument(Exception ex){
return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(
ResultUtil.custom(HttpStatus.BAD_REQUEST.value(), ex.getMessage())
);
} /**
* 接口参数校验异常
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity handleArguNotValid(MethodArgumentNotValidException ex){
FieldError fieldError=ex.getBindingResult().getFieldErrors().get(0);
String msg = !StringUtils.isEmpty(fieldError.getDefaultMessage()) ? fieldError.getDefaultMessage():"参数不合法";
return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(
ResultUtil.custom(HttpStatus.BAD_REQUEST.value(), msg)
);
} /**
* 参数不合法异常
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseEntity handleConstraintViolation(ConstraintViolationException ex){
String err=ex.getMessage();
Set<ConstraintViolation<?>> set=ex.getConstraintViolations();
if(!set.isEmpty()){
err= set.iterator().next().getMessage();
}
String msg = StringUtils.isEmpty(err)?"参数不合法":err;
return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(
ResultUtil.custom(HttpStatus.BAD_REQUEST.value(), msg)
);
} /**
* 参数不合法异常
*/
@ExceptionHandler(value = {IllegalArgumentException.class})
public ResponseEntity handleIllegalArgu(Exception ex){
String err=ex.getMessage();
String msg = StringUtils.isEmpty(err)?"参数不合法":err;
return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(
ResultUtil.custom(HttpStatus.BAD_REQUEST.value(), msg)
);
} /**
* 业务逻辑处理异常,也是我们最常用的主动抛出的异常。
*/
@ExceptionHandler(value = BusinessException.class)
public ResponseEntity handleBadBusiness(Exception ex){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body(
ResultUtil.custom(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage())
);
} /**
* HTTP请求方法不支持异常
*/
@ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
public ResponseEntity methodNotSupportException(Exception ex){
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED.value()).body(
ResultUtil.custom(HttpStatus.METHOD_NOT_ALLOWED.value(), "请求方法不支持!")
);
} /**
* 除上面以外所有其他异常的处理会进入这里
*/
@ExceptionHandler(value = Exception.class)
public ResponseEntity handleException(Exception ex){
log.error("[ExceptoinTranslator]>>>> 全局异常: ", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body(
ResultUtil.custom(HttpStatus.INTERNAL_SERVER_ERROR.value(), "发生内部错误!")
);
} }

上面这个全局异常处理,包含了项目最有可能出现的:几种参数异常、权限异常、HTTP方法不支持异常、自定义业务异常、其他异常,基本上够用了,如果还想更细致一点还可以自定义其他的异常放进来。

这里要关注的两点是:

1、我们统一使用Spring的ResponseEntity进行了外层包装,而不是直接使用自定义响应对象ResultUtil来返回,这样保证了我们接口返回的业务状态和接口本身的HTTP状态是一致的,前端就可以判断接口连通性了,如果不明白区别,使用一下Postman就可以看到右上角的HTTP状态了,你使用自定义响应对象返回时永远都是200;

2、最后其他所有异常Exception.class的捕获,务必进行log.error(ex)日志记录,这样线上排查时才能看到具体的堆栈信息。

总结

  1. 合理利用stream分组提高查询效率;

  2. stream排序避免踩坑;

  3. 异步线程最佳用法;

  4. 统一异常管理最佳使用方式。


本人原创文章纯手打,大多来源于工作,觉得有一滴滴帮助就一键四连吧!

点个关注,不再迷路!

点个收藏,不再彷徨!

点个推荐,梦想实现!

点个赞,天天赚!


【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家的更多相关文章

  1. CAS (15) — CAS 线上环境 Ehcache Replication 的非稳定重现错误 java.util.ConcurrentModificationException

    CAS (15) - CAS 线上环境 Ehcache Replication 的非稳定重现错误 摘要 线上环境在 EhCache Replication 过程中出现 java.util.Concur ...

  2. 使用Fabric一键批量部署上线/线上环境监控

    本文讲述如何使用fabric进行批量部署上线的功能 这个功能对于小应用,可以避免开发部署上线的平台,或者使用linux expect开发不优雅的代码. 前提条件: 1.运行fabric脚本的机器和其他 ...

  3. 【微信小程序】---线上环境搭建

    一.前言 通常我们在本地电脑上开发微信小程序,调用和访问小程序会有很多问题.特别是在配有自己后端的情况下,我们通过真机访问我们的小程序会出现不可访问的问题 二.线上环境搭建 在这里我们主要以腾讯云给大 ...

  4. robot framework 测试/预发/线上环境快捷切换

    通常情况下布署的三套环境:测试.预发及线上环境.调试或者辅助验证测试时,切环境改变量甚是麻烦.这些变量包括但不限于:一些url信息,数据库信息,预置用户信息等. 切换环境方法一:使用变量文件,通过判断 ...

  5. 线上环境HBASE-1.2.0出现oldWALs无法自动回收情况;

    正常情况下,hmaster会定期清理oldWALs文件夹,一般该文件大小也就几百兆,但是我们线上 环境出现了该文件没有自动回收情况,如图: 该目录占用hdfs空间多达7.6T,浪费空间: 后来经过多番 ...

  6. vue本地和线上环境(域名)配置

    vue本身为运行脚手架项目自家搭载了一个nodejs后台环境,本地可通过proxyTable来处理跨域问题,但是上线(或生产环境)之后改域名真是一件麻烦的事情,所以进行一些配置. config/ind ...

  7. wechat开发笔记之1.线上环境搭建与测试

    Wechat开发笔记 线上环境搭建: 申请一个wechat公众平台. 手机个人微信可以用webwechat来测试. Website:https://web.weixin.qq.com/ 手机客户端扫一 ...

  8. Vue 2.x 3.x 配置项目开发环境跟线上环境

    先找到package.json  (这是nuxt版的vue 可能会跟一般vue不一样  当然总体上差不多的) "scripts": { "dev": " ...

  9. Docker + node(koa) + nginx + mysql 线上环境部署

    在上一篇 Docker + node(koa) + nginx + mysql 开发环境搭建,我们进行了本地开发环境搭建 现在我们就来开始线上环境部署 如果本地环境搭建没有什么问题,那么线上部署的配置 ...

随机推荐

  1. metinfo 6.0 任意文件读取漏洞

    一. 启动环境 1.双击运行桌面phpstudy.exe软件 2.点击启动按钮,启动服务器环境 二.代码审计 1.双击启动桌面Seay源代码审计系统软件 2.点击新建项目按钮,弹出对画框中选择(C:\ ...

  2. Flask(Jinja2) 服务端模板注入漏洞

    原理 参考文章: https://www.blackhat.com/docs/us-15/materials/us-15-Kettle-Server-Side-Template-Injection-R ...

  3. @Autowired 注解有什么用?

    @Autowired 可以更准确地控制应该在何处以及如何进行自动装配.此注解用于在 setter 方法,构造函数,具有任意名称或多个参数的属性或方法上自动装配bean.默认情况下,它是类型驱动的注入. ...

  4. 转载:23种常用设计模式的UML类图

    转载至:https://www.cnblogs.com/zytrue/p/8484806.html 23种常用设计模式的UML类图 本文UML类图参考<Head First 设计模式>(源 ...

  5. elasticsearch 是如何实现 master 选举的 ?

    想了解 ES 集群的底层原理,不再只关注业务层面了. 前置前提: 1.只有候选主节点(master:true)的节点才能成为主节点. 2.最小主节点数(min_master_nodes)的目的是防止脑 ...

  6. 学习Puppet(三)

    一.相关概念: 1.  puppet基于C/S架构,使用ruby编写,在类UNIX平台上集中配置管理系统,它可以管理配置文件.用户.cron任务.软件包.系统服务. 2.  puppet把系统实体称为 ...

  7. 为什么HTTP/3要基于UDP?可靠吗?

    目录 前言 为什么转用UDP? HTTP/3解决了那些问题? 队头阻塞问题 QPACK编码 Header 参考 推荐阅读: 计算机网络汇总 HTTP/3竟然是基于UDP的!开始我也很疑惑,UDP传输不 ...

  8. css文字颜色渐变的3种实现

    在web前端开发过程中,UI设计师经常会设计一些带渐变文字的设计图,在以前我们只能用png的图片来代替文字,今天可以实现使用纯CSS实现渐变文字了.下面就介绍3中实现方式供大家参考! 基础样式: .g ...

  9. Vue小说阅读器(仿追书神器)

    一个vue阅读器项目,目前已升级到2.0,阅读器支持横向分页并滑动翻页(没有动画,需要动画的可以自己设置,增加transitionDuration即可) 技术栈 vue全家桶+mint-ui gith ...

  10. 用CSS实现Tab页切换效果

    用CSS实现Tab切换效果 最近切一个页面的时候涉及到了一个tab切换的部分,因为不想用js想着能不能用纯CSS的选择器来实现切换效果.搜了一下大致有下面三种写法. 利用:hover选择器 缺点:只有 ...