1

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

 2 years ago
source link: https://www.cnblogs.com/fulongyuanjushi/p/16153733.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家 - 福隆苑居士 - 博客园

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


经过了接近两周的维护,除了日常问题以外,代码层面我一共处理了一个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. 统一异常管理最佳使用方式。


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

点个关注,不再迷路!

点个收藏,不再彷徨!

点个推荐,梦想实现!

点个赞,天天赚!

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK