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

前言

因为新项目前段时间已经完成,现在已经稳定,所以我最近被分配到公司运维团队,负责维护另一个项目,包括处理客户反馈的日常问题,优化系统缺陷。

经过近两周的维护,除了日常的问题,我还处理了代码中的一个bug,优化了三个问题。我将这四个问题总结为四个编码技巧与大家分享。我希望它会有所帮助。以后如果遇到类似的问题,可以来找我查一下。它必须节省很多时间。

提示1、分组

很多人都知道java8很好用,但是很多人并没有真正用过,或者查了很多资料还是用不上。理解。这里我从在线项目中提取了一个分组的代码片段,帮助大家一目了然。

首先我会展示表结构,当然是为了简化案例以便于理解。

1

张三

2

李斯

西部

1

东校区

2

南校区

3

西部

西校区

4

北校区

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

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

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

可以看出,他遍历医生列表,然后查询每个医生所在的院区名称并返回,也就是说如果有100位医生,那么院区表需要查询了100次,虽然.0+其实以后查询效率变高了。这种小表查询其实影响不大,但是作为一个成熟的在线项目,这种代码是新手级别的。相信很多人都这么写过。

优化后:

// 查询医生列表
List doctorVOList = doctorService.findDoctorList();
// 以areaCode为key将院区列表分组放入内存中
Map<String,List> 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();

图片[1]-【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家-唐朝资源网

// 放入院区名称 vo.setAreaName(areaName); }); // 返回 return doctorVOList;

可以看出,医院区域的信息根据医院区域编码为key,医院区域的信息作为值放入内存,然后在遍历医生列表时,直接从内存中检索对应的医院区号 医院区名就够了,前后只查询一次,大大提高了效率,节省了数据库资源。

每当像这样的遍历查询需要从其他小表中找出某个属性的值时,都可以使用此方法。

2、排序

这种排序其实很简单。就是按照客户要求的多条规则对医生名单进行排序。这里的规则是:按照是否在线降序,按照医生的职称和医生编号升序。

项目中使用,所以之前的写法是直接写SQL语句,但是如果SQL语句比较复杂,以后给其他同事维护也不好。

其实找到列表后,直接在内存中排序是很舒服的,所以我在这部分项目中优化了SQL语句的编写,直接在代码中查询排序。

多个属性按不同的规则排序:

// 查询列表
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了,很简单,()表示降序,不写则表示默认升序。

这里需要注意的是,网上很多资料都是有用的:

(TO::).()

这种降序方式被误解了。可以具体查看或者试试()的用法,它只是倒序而不是降序,类似于从左到右到从右到左的形式,降序必须使用上面的代码写法,这是一个要注意的坑.

3、异步线程

很多人都知道可以直接用@Async注解来使用异步线程,但是很多人不知道使用这个注解的局限性。

@Async 注解只能标记在 void 方法上;必须修改带有@Async 注解的方法; @Async注解标记的方法和调用者在同一个类中,不会生效。

以上条件缺一不可,即使满足前两个,也不会使用异步线程。

我维护的这个项目只是满足了前两个,但实际上并没有生效。说明写这段代码的同事思路不错。希望不要占用主线程来提高界面效率,但其实我自己也没有完全测试过。 ,认为它是有效的,我相信很多人都这样做了。

这里,我优化了,给你最科学的写法,保证有效。这里我以短信通知为例。

首先,定义一个专门为编写异步方法而调用的类。

/**
 * 异步方法的服务, 不影响主程序运行。
 */
@Service
public class AsyncService {
    private final Logger log = LoggerFactory.getLogger(AsyncService.class);
    
    @Autowired
    private PhoneService phoneService;
    
    /**
     * 发短信通知患者检查时间
     * @param dto 患者信息
     * @param consult 咨询信息
     */
    @Async

图片[2]-【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家-唐朝资源网

    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);     } }

这里注意,修饰符的使用,void方法,在之前的限制中已经提到过。

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

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

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

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

这样发送短信的业务会走一个异步线程,即使有其他类似的业务需要异步调用,也都可以放在里面统一处理。

我们还要注意,上述方法中的异步线程实际上使用的是默认线程池,不推荐使用默认线程池,因为在重度使用过程中线程数可能不够导致阻塞,所以我们为了进一步优化,使用自定义的线程池。

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

@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–开头。

其实这些配置都可以提取出来放到yml文件中。具体配置要根据使用异步线程的项目规模和服务器本身的水平来判断。我们项目中用到异步线程的地方并不多。在发送短信通知和订阅消息通知时,服务器本身是8核16G,这个设置比较一致。

4、统一异常管理

统一异常管理是我要讲的。这次我在维护一个项目,在这部分写不下去了。我在在线故障排除中看不到很多重要信息。我检查了代码,发现它显然被使用了。统一的异常管理,但是写法简直是外行水平,一生气就肚子疼。

首先说一下规范:

统一异常管理后,如果没有必要,千万不要try…catch。如果一定要try…catch,一定要log.error(e)记录日志并打印堆栈信息,并抛出异常,否则代码块在问题行看不到任何东西;

统一异常管理后,在接口级别检查错误时,不要直接使用通用响应对象返回,如.error(500, “query xx “),这样会导致统一异常管理丢失有效性,因为这是正常返回一个对象不是异常,所以我们应该在验证错误发生时主动抛出一个异常(“query xx ”),这样才会被捕获;

统一异常管理后,最好使用全局异常管理类中内置的封装层,保证HTTP状态不是200,而是正确的异常状态,以便前端工程师判断根据 HTTP 状态的接口连接。然后根据业务状态判断接口是否获取数据成功。

这里贴出项目中针对全局异常优化的统一处理代码与大家分享:

首先,我们自定义了三个常见的异常。

验证参数异常,继承运行时异常。

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

检查权限异常,继承运行时异常。

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

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

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

图片[3]-【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家-唐朝资源网

} }

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

/**
* 统一异常处理
*/
@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)

图片[4]-【Java分享客栈】从线上环境摘取了四个代码优化记录分享给大家-唐朝资源网

); } /** * 参数不合法异常 */ @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、我们统一使用外包装,而不是直接使用自定义的响应对象返回,保证了我们接口返回的业务状态和接口本身的HTTP状态一致,前端可以判断接口的连通性。如果你不明白其中的区别,你可以使用它来查看右上角的 HTTP 状态。使用自定义响应对象返回时,始终为200;

2、最后,要捕获所有其他的.class,一定要log.error(ex),这样在线排查时可以看到具体的堆栈信息。

总结

合理使用分组,提高查询效率;

排序避免踩坑;

异步线程的最佳使用;

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

我的原创文章纯手写,大部分来自工作。如果您觉得有帮助,只需单击并一键连接四次!

点击关注,别再迷路了!

© 版权声明
THE END
喜欢就支持一下吧
点赞236赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容