【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家

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

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

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

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

  • 医生信息表

id doctor_name phone photo_url area_code 1 张三 13612345678

EAST 2 李四 15845678901

WEST

  • 院区表

id area_code area_name 1 EAST 东院区 2 SOUTH 南院区 3 WEST 西院区 4 NORTH 北院区

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

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

// 查询医生列表
List 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 doctorVOList = doctorService.findDoctorList();

// 以areaCode为key将院区列表分组放入内存中
Map> areaMap = areaService.findAll().stream()
            .collect(Collectors.groupingBy(e-> e.getAreaCode()));

// 遍历医生列表,装入院区名称。
List 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次,极大提高了效率,节省了数据库资源。

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

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

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

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

// 查询列表
List respDTOList = findHomePageDoctorList();

// 排序
List 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()的用法,它只是反转不是降序排列,类似于从左到右变为从右到左这样的形式,降序一定要用上面代码的写法,这是一个要注意的坑。

异步线程很多人都知道,直接使用@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,所以这个设置是相对符合的。

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

首先,我说一下规范:

  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> 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. 统一异常管理最佳使用方式。

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

点个关注,不再迷路!

点个收藏,不再彷徨!

点个推荐,梦想实现!

点个赞,天天赚!

Original: https://www.cnblogs.com/fulongyuanjushi/p/16153733.html
Author: 程序员济颠
Title: 【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/593039/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • JAVA生产者消费者的实现

    春节回了趟老家,又体验了一次流水席,由于桌席多,导致上菜慢,于是在等待间,总结了一下出菜流程的几个特点: 1.有多个灶台,多个灶台都在同时做菜出来。 2.做出来的菜,会有专人用一个…

    Java 2023年5月29日
    055
  • WebSocket 服务端未启动时,客户端重连报错

    当WebSocket服务端未启动时,我们在客户端申请连接,会报 System.Net.Sockets.SocketException 异常。 当然,我们调试时异常设置默认是不勾选这…

    Java 2023年5月30日
    062
  • 蜻蜓点水说说Redis的String的奥秘

    本篇博客参考:掘金Redis小册 敖丙 如果面试官问你,单线程的Redis为什么那么快,你可能脱口而出,因为单线程,避免上下文切换;因为基于内存,比硬盘读写快很多;因为采用的是多路…

    Java 2023年6月5日
    070
  • [推荐]趣味剖析Spring5核心原理

    课程亮点; ●你想了解面试中碰到的spring的各种面试问题及背后的原理吗? ●你想知道如何在繁冗复杂的源码中揭开spring IoC的面纱吗? ●你想知道如何在拗口,晦涩的术语中…

    Java 2023年5月29日
    056
  • Rocket Mq 常用API 及简单运维

    RocketMQ 常用API 消息 消息消费模式 消息消费模式由消费者来决定,可以由消费者设置MessageModel来决定消息模式。 消息模式默认为集群消费模式 consumer…

    Java 2023年6月7日
    074
  • java利用Tesseract 识别身份证号码

    安装Tesseract http://blog.csdn.net/hiredme/article/details/50894814 http://blog.csdn.net/yoa…

    Java 2023年5月29日
    070
  • Google支付和服务端验证

    因为公司业务需求,需要使用google的登录和支付。google支付分为订阅和应用内购买两种,笔者使用的是应用内购买这种方式,这里将整个google支付和支付验证的流程记录下来。 …

    Java 2023年5月30日
    070
  • 如何高效的在博客园上编写MD格式的博客(插件pycnblog,推荐)

    如何高效的在博客园上编写MD格式的博客 想要找一个随时可以查看自己的学习笔记,将笔记放在博客园是一个很不错的选择,但博客园本身的后台写还是很不方便。写MD格式的文档我还是喜欢使用 …

    Java 2023年5月30日
    094
  • 代码都写不完,还写个锤子注释!

    现在的项目开发里,代码注释就像程序员的头发,越来越少。 尤其是国内,这种现象不仅是在小公司小团队中司空见惯,就算在大公司,以及大团队中的开源项目里,也是屡见不鲜。 上图是我在阿里的…

    Java 2023年6月7日
    073
  • java反射与注解

    我们都知道,计算机运行代码,需要经过编译-运行这两个步骤,而反射就是当程序运行状态时,通过类名,就知道这个类有什么属性,有什么方法在里面.简而言之,在 Java 中,只要给定类的名…

    Java 2023年6月8日
    044
  • Nginx(一)-windows下的安装配置

    第一步 下载 官网下载地址 因为只是测试这里选择最新版本1.13.9 下载完成得到zip压缩包 解压后得到如下目录 第二步 启动nginx 注意不要直接双击nginx.exe,这样…

    Java 2023年5月30日
    057
  • 【RocketMQ】消息的刷盘机制

    刷盘策略 CommitLog的 asyncPutMessage方法中可以看到在写入消息之后,调用了 submitFlushRequest方法执行刷盘策略: public class…

    Java 2023年6月8日
    071
  • Kubernetes 1.12公布:Kubelet TLS Bootstrap与Azure虚拟机规模集(VMSS)迎来通用版本号

    今天,我们非常高兴地推出Kubernetes 1.12版本号,这也是我们今年以来公布的第三个版本号。 此次公布继续关注内部改进与功能完好,旨在进一步提升与Kubernetes对接时…

    Java 2023年5月30日
    069
  • 灵感乍现!造了个与众不同的Dubbo注册中心扩展轮子

    hello大家好呀,我是小楼。 作为一名基础组件开发,服务好每一位业务开发同学是我们的义务(KPI)。 客服群里经常有业务开发同学丢来一段代码、一个报错,而我们,当然要微笑服务,耐…

    Java 2023年6月6日
    080
  • 2.搭建一个spring-boot项目(持续更新)

    很多同学在搭建一个springboot项目的时候会遇到很多问题,闲来无事我就自己搭建了一个基础的框架,大家可以自己看看。 框架主要包括: 初始化配置 数据库配置 Mysql myb…

    Java 2023年6月9日
    080
  • 到底什么是.NET?

    ​.NET 概念比较庞大,本文只讨论基础知识,只用简单语言描述。 我们是NET程序员, 但是我们有没有思考过到底什么是.NET ? 官方定义 .NET是微软推出来的一个致力于敏捷开…

    Java 2023年6月9日
    073
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球