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、自定义异常类与全局异常处理:
对于外部调用API异常,应该报错503 Service Unavailable,同时在全局异常处理的时候添加对与外部服务异常处理的方法。