Try-catch思考

52 阅读4分钟

1、调用外部API服务时候的try-catch

/**
 * 获取指定年份的所有工作日
 */
@Override
public Set<String> getWorkdaysOfYear(int year) {
    // 先查缓存
    if (workdaysCache.containsKey(year)) {
        log.debug("从缓存获取{}年工作日数据", year);
        return workdaysCache.get(year);
    }

    Set<String> workdays = new HashSet<>();

    try {
        String url = "https://timor.tech/api/holiday/year/" + year;
        log.info("调用API获取{}年工作日: {}", year, url);
        String response = restTemplate.getForObject(url, String.class);
        // 将JSON转化为树形结构好遍历
        JsonNode root = objectMapper.readTree(response);

        if (root.get("code").asInt() != 0) {
            log.warn("API返回失败,code: {}", root.get("code"));
            throw new DataValidationException("API获取的JSON异常");
        }

        JsonNode holiday = root.path("holiday");
        log.info("API返回{}年节假日数据,共{}条", year, holiday.size());

        // 将API返回的节假日数据转换为完整日期格式的Map
        // API格式: "01-01" -> 转换为 "2025-01-01"
        Map<String, Boolean> holidayMap = new HashMap<>();
        holiday.fields().forEachRemaining(entry -> {
            String shortDate = entry.getKey();  // 格式: "01-01"
            JsonNode dateInfo = entry.getValue();
            boolean isHoliday = dateInfo.path("holiday").asBoolean(false);
            String fullDate = year + "-" + shortDate;  // "2025-01-01"
            holidayMap.put(fullDate, isHoliday);

        });

        // 遍历全年的每一天
        Calendar calendar = Calendar.getInstance();
        calendar.set(year, Calendar.JANUARY, 1);

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

        int holidayCount = 0;
        int weekendCount = 0;
        int workdayCount = 0;

        while (calendar.get(Calendar.YEAR) == year) {
            String dateStr = sdf.format(calendar.getTime());
            int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
            boolean isWeekend = (dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY);

            // 检查这一天是否在holiday数据中
            if (holidayMap.containsKey(dateStr)) {
                boolean isHoliday = holidayMap.get(dateStr);

                if (isHoliday) {
                    holidayCount++;
                } else {
                    workdays.add(dateStr);
                    workdayCount++;
                }
            } else {
                if (isWeekend) {
                    weekendCount++;
                } else {
                    workdays.add(dateStr);
                    workdayCount++;
                }
            }
            calendar.add(Calendar.DAY_OF_MONTH, 1);
        }

        log.info(" {}年统计 - 工作日:{}天, 节假日:{}天, 周末:{}天",
                year, workdayCount, holidayCount, weekendCount);

        workdaysCache.put(year, workdays);

    } catch (RestClientException e) {
        log.error("调用节假日API失败,URL可能错误或网络不通: {}", e.getMessage(), e);
        throw new ExternalServiceException("API获取节假日数据异常",e);
    }catch(DataValidationException e){
        log.error("获取{}年工作日失败: {}", year, e.getMessage());
        throw e;
    } catch (Exception e){
        log.error("处理{}年节假日数据时发生未知错误: {}", year, e.getClass().getName(), e);
        throw new ExternalServiceException("未知错误:处理节假日数据失败",e);
    }
    return workdays;
}

该方法用于调取外部API获取工作日,但是该API网站返回的JSON格式固定,所以存在以下问题:

1、DataValidationException的catch多余,接住往上抛,但是上层也知道肯定是你API调取的问题,同样RestClientException也没必要,只需要留存Exception就行,让上层知道是你API调取出错。

2、Try-catch太长,只有调取API的时候会出错,后面的遍历calendar和集合处理一般都不会出错,所以拆分为几个方法最好,原有方法不变getWorkdayofYear 不变,后面拆分一个调取API的方法fetchHolidayDataFromApi和结算节假日的calculateWorkdays方法出来。

2、Service层的try-catch

主方法与子方法的思考

原有代码:

    public List<SelectUserDTO> queryFromExcel(MultipartFile file, SelectUserQuery query) {
        try {
            List<DailyReportRecord> allReports = parseExcel(file);
            List<EmployeeInfo> employees = parseEmployeeSheet(file,0);
            log.info("共解析到{}条日报记录,{}名员工信息", allReports.size(), employees.size());

            List<Date> workdays = getWorkdaysBetween(
                    query.getQueryTimeStart(),
                    query.getQueryTimeEnd()
            );

            log.info("查询时间范围: {} 至 {}, 共{}个工作日",
                    formatDate(query.getQueryTimeStart()),
                    formatDate(query.getQueryTimeEnd()),
                    workdays.size());

            List<SelectUserDTO> result = new ArrayList<>();

            for (Date workday : workdays) {
                // 当天写了日报的用户
                Set<String> writtenUsers = allReports.stream()
                        .filter(r ->(r.getDate().equals(workday)))
                        .map(DailyReportRecord::getUsername)
                        .collect(Collectors.toSet());

                // 找出当天在职但未写日报的用户
                for (EmployeeInfo employee : employees) {
                    if (employee.isActiveOn(workday) && !writtenUsers.contains(employee.getUsername())) {
                        SelectUserDTO dto = new SelectUserDTO();
                        dto.setUsername(employee.getUsername());
                        dto.setUndoTime(workday);
                        result.add(dto);
                    }
                }
            }

            return result;
        } catch (FileProcessException e) {
            log.error("文件处理失败: {}", e.getMessage());
            throw e;
        } catch (DataValidationException e) {
            log.error("数据验证异常:Excel数据格式错误 或 API返回数据错误: {}", e.getMessage());
            throw e;
        } catch (ExternalServiceException e){
            log.error("外部服务异常:节假日API调用失败: {}", e.getMessage());
            throw e;
        } catch (Exception e) {
            log.error("查询未写日报用户时发生未知错误", e);
            throw new FileProcessException("未知错误:查询未写日报用户时: " + e.getMessage(), e);
        }
    }
    /**
     * 解析日报表 -使用hutools
     *
     */
    private List<DailyReportRecord> parseExcel(MultipartFile file) {
        try (InputStream is = file.getInputStream()) {
            ExcelReader reader = ExcelUtil.getReader(is, "日报");
            Map<String, String> headerAliasMap = new HashMap<>();
            headerAliasMap.put("姓名", "username");
            headerAliasMap.put("日期", "date");
            headerAliasMap.put("工作内容", "content");
            reader.setHeaderAlias(headerAliasMap);
            List<DailyReportRecord> records = reader.readAll(DailyReportRecord.class);
            // 过滤空数据
            records = records.stream()
                    .filter(r -> StringUtils.isNotBlank(r.getUsername()))
                    .collect(Collectors.toList());

            if (records.isEmpty()) {
                throw new DataValidationException("日报表中没有有效数据");
            }
            return records;
        } catch (IOException e) {
            log.error("读取日报表失败", e);
            throw new FileProcessException("IO错误:无法读取日报表", e);
        } catch (DataValidationException e) {
            throw e;
        } catch (Exception e) {
            log.error("解析日报表失败", e);
            throw new FileProcessException("未知错误:解析日报表失败: " + e.getMessage(), e);
        }
    }

    /**
     * 解析人员信息表---使用hutools
     */
    @Override
    public List<EmployeeInfo> parseEmployeeSheet(MultipartFile file,Integer type) {
        try (InputStream is = file.getInputStream()) {
            ExcelReader reader = ExcelUtil.getReader(is, "人员信息");
            Map<String, String> headerAliasMap = new HashMap<>();
            headerAliasMap.put("人员", "username");
            headerAliasMap.put("入职日期", "hireDate");
            headerAliasMap.put("离职日期", "resignationDate");
            headerAliasMap.put("一级业务", "firstLevelBusiness");
            reader.setHeaderAlias(headerAliasMap);
            List<EmployeeInfo> employees = reader.readAll(EmployeeInfo.class);
            if(type==0){
                employees = employees.stream()
                        .filter(e -> StringUtils.isNotBlank(e.getUsername()))
                        .collect(Collectors.toList());
            } else if (type==1) {
                employees = employees.stream()
                        .filter(e -> StringUtils.isNotBlank(e.getUsername()))
                        .filter(e -> "自建应用".equals(e.getFirstLevelBusiness()))
                        .collect(Collectors.toList());
            }else{
                employees = employees.stream()
                        .filter(e -> StringUtils.isNotBlank(e.getUsername()))
                        .filter(e -> "平台复用".equals(e.getFirstLevelBusiness()))
                        .collect(Collectors.toList());
            }

            if (employees.isEmpty()) {
                throw new DataValidationException("人员信息表中没有有效数据");
            }
            return employees;

        } catch (IOException e) {
            throw new FileProcessException("IO错误:无法读取人员信息表", e);
        } catch (DataValidationException e) {
            throw e;
        } catch (Exception e) {
            throw new FileProcessException("未知错误:解析人员信息表失败: " + e.getMessage(), e);
        }
    }

存在问题:

1、service层主方法不应该catch这么多异常,应该交给controller全局异常处理,可以把主方法的try-catch全去掉

2、子方法的多重catch也没必要,在catch里面只转换技术异常。

2.1、在方法内部抛出了DataValidationException,就不应该catch让它自然抛了(虽然这是为了怕catch Exception给捕获)。 2.2、没必要catch Exception,出现未知错误比如空指针异常,应该让它自然往上抛,对于file.getInputStream()必须catch IO异常。

2.3、DataValidationException需要转换一下抛出,因为如果不抛出返回空列表,会影响上层方法输出所有人都没有写日报,不符合业务设计。

3、自定义异常类与全局异常处理:

image.png 对于外部调用API异常,应该报错503 Service Unavailable,同时在全局异常处理的时候添加对与外部服务异常处理的方法。

image.png