maven项目结构遵从DDD整洁架构分为如下四个顶级包:
application - 应用层代码,一般为接口层定义API的实现类和一些结构转化,application不应该承载业务逻辑
domain - 领域层,包含应用的业务模型定义,全部业务逻辑,可以细分实体(entity)和领域服务(service)等子包
infrastructure - 基础设施层,包含配置、基础工具、切面、枚举、外部服务调用、消息、缓存等中间件
interfaces - 服务自身API的定义,以及与API定义相关的API结构(DTO)定义
单模块工程结构示例:
assignment-service
– src/main/java
---- com.huawei.it.hr.assignment
------ application
------ domain
------ infrastructure
------ interfaces
------ AssignmentApp.java 启动类
– pom.xml
多模块工程结构示例:
MetadaPayrollCalculation
– PayrollCalcService
---- src/main/java
------ com.huawei.it.hr.payroll
------ application
------ domain
------ infrastructure
------ interfaces
---- pom.xml 子模块构建配置文件
– PayrollCalcBase
---- src/main/java
------ com.huawei.it.hr.payroll
------ application
------ domain
------ infrastructure
------ interfaces
---- pom.xml 子模块构建配置文件
– pom.xml 父模块构建配置文件
各层级依赖关系为:
接口层通常不依赖应用层/领域层,可以依赖部分基础设施层工具、类型
应用层依赖接口层的API接口定义,并依据接口定义提供实现
应用层依赖领域层处理业务逻辑
应用层可以使用部分基础设施层工具、类型
领域层不反向依赖应用层和接口层。可以依赖部分基础设施层工具和类型
说明: Java源代码编译成class文件时,会保留一些公共部分,如类名,公共方法名等。但是JVM出于一些优化考虑,会调整一些细节部分,如将内部常量,变量,方法内联,省略形参变量名(仅保留类型),包装异常块等。因此,根据class文件反向编译出来的源代码,与生成class的原始源代码大相径庭。在代码中直接使用反向编译生成的勉强能工作的源代码,严重影响可读性,后续也极难维护,应该杜绝。
错误示例:
try {
try {
Object[] result = BusinessDataWriter.save(dataEntities[0].getDataEntityType(), dataEntities,
option);
if (Configuration.isEnable()) {
DTXServiceHelper.confirmXid(dbkey,
result != null ? DynamicObjectSerializeUtil.serialize(result,
dataEntities[0].getDynamicObjectType()) : null);
}
RecordSaveFormServiceHelper.recordSaveFormToCache(
dataEntities[0].getDataEntityType().getName());
var7 = result;
} catch (Throwable var17) {
throw var17;
}
} catch (Throwable var18) {
throw var18;
}
return var7;
#### 2.2 禁止拷贝第三方代码,需要引用/修改时应显式声明依赖后扩展
说明:直接拷贝第三方/开源代码,有版权问题,且直接拷贝源码,失去了后续迭代的一切可能性,在出现漏洞/升级时补救成本巨大。按照Open-Close原则,应该将依赖显式声明,在需要修改依赖包API行为时,使用装饰器,转换器等模式进行扩展。直接拷贝其他工程的代码,也及容易留下无用代码和产生重复代码,应该禁止。
### 3. 命名和分包约束
#### 3.1 类型、变量和方法使用合适的命名
说明:类型,变量,方法签名必须具备一定的自解释性,也是代码可读性的重要基础。在《阿里巴巴编码规范》基础上,禁止使用 map,object, json, data, list 等非常泛化的命名
#### 3.2 数据库映射对象(实体)命名以Entity/Pojo结尾,如 CalcPersonEntity/CalcPersonPojo
对于数据库映射对象,统一使用 *Entity/Pojo 的命名风格,放在 entity/pojo 包下
#### 3.3 API结构定义对象 命名以Dto结尾,如 CalcPersonDto
对于API结构定义类对象,统一使用 *Dto 的命名风格(数据传输对象)
#### 3.4 领域层一般模型,直接使用语义命名,放在 model 包下
领域层实体模型,按照3.2放在entity包下,其他非实体领域层模型,放在model包下
#### 3.5 领域层一般模型,不建议保留古老的 VO/BO 等包名和命名后缀
VO原意至Value Object 或 View Object。值对象的含义已由Dto概括,View Object 在前后端分离架构中通常已经不复存在。BO 原意为 Business Object,名称太泛,不建议使用。
#### 3.6 数据库查询类非持久化结构,使用 Query 结尾,如 CalPersonQuery,对于事务类结构,建议使用Command结尾或直接语义化命名
例如根据业务需要定制的查询参数结构,建议使用 *Query 的命名风格。对于事务类操作,可以使用*Command的命名风格,也可以直接按照上下文语义进行命名。
#### 3.7 贯穿接口/领域层的枚举类型,放在 infrastructure 层定义,领域层内部的枚举,可以直接定义在 domain 层 model 包中
对于枚举类型,通常不适合在接口层和领域层重复定义,为保持层次依赖关系清晰,共用的枚举结构建议定义在基础设施层中作为公共结构。领域层专用枚举,可以直接放在domain层model包下。
### 4 代码结构约定
#### 4.1 禁止使用动态类型描述固定结构
说明:JAVA是面向对象的语言,也是JAVA代码保持良好延展性的重要基石。在描述格式固定的结构时,使用类型和对象非常合适,可以有效的进行语义化表达和类型约束,让更多的错误在编译阶段即可识别和解决。而不至于留到运行时。Json,Map等动态类型仅在描述真正不确定的动态结构时是合理的,但是如果结构较为固定(如获取/设置固定字符串描述的Key),使用动态类型表达会绕过一些编译期检查,同时往往伴随着诸多类型转换,降低了代码可读性,重用性和健壮性。
错误示例:
Map param = new HashMap();
param.put("appId", ConfigPropertiesUtil.getContextProperty("application.appId"));
param.put("subAppId", ConfigPropertiesUtil.getContextProperty("application.subAppId"));
param.put("jobWorkerDuId", ConfigPropertiesUtil.getContextProperty("lite.job.client.jobworkerduid"));
param.put("jobName", ConfigPropertiesUtil.getContextProperty("lite.job.client.jobname"));
param.put("taskName", ConfigPropertiesUtil.getContextProperty("lite.job.client.taskname"));
JSONObject obj = new JSONObject(param);
// 此处Map中的结构是固定的,使用一个固定结构更为合适。
对比示例:
@Getter
@Setter
public class JobConfig {
private String appId;
private String subAppId;
private String jobWorkerDuId;
private String jobName;
private String taskName;
}
#### 4.2 代码块合理分段,确保结构清晰,避免出现过多代码块缩进嵌套
箭头函数是一个临时定义的方法,缺省了方法签名,在内容较少时比较适用。箭头函数的代码块中不宜存放过多代码内容,会导致更多复杂的缩进和嵌套,不利于理解。如果代码块中需要执行的指令较多,应该提取为单独的方法,进行合适的方法署名。
错误示例:
list.stream().forEach(beforeVO -> {
try {
LogRecordVo logRecordVo = LogRecordVo.builder().tableName(PayElementAttributeDomain.LOG_TABLE_NAME)
.entities(Arrays.asList(PayElementAttributeDomain.LOG_TABLE_NAME)).moduleName(PayElementAttributeDomain.LOG_MODULE_NAME_DELETE).businessNo(String.valueOf(beforeVO.getAttributeId()))
.operationEnum(LogOperationEnum.DELETE).beforeObject(beforeVO).afterObject(null).build();
AsyncMessage asyncMessage = new AsyncMessage(LogRecordConstant.MSG_LOG_RECORD_SEND);
asyncMessage.setContent(logRecordVo);
messageSender.send(asyncMessage);
} catch (Exception e) {
log.error("log error: {}", e.getMessage());
}
});
等价示例:
list.forEach(beforeVO -> sendMessage(beforeVO));
private void sendMessage(PayElementAttributeBusinessViewDto vo) {
try {
// … 原始发送消息相关代码
} catch (Exception exception) {
log.error(“log error: {}”, e.getMessage());
}
}
#### 4.3 基于SOLID原则,使用组合优于继承
SOLID原则包括的内容很多,其基本含义是职责单一(Single Responsibility),对扩展开放,对修改关闭(Open Close), 里氏替换(Interface Segregation),接口隔离(Interface Segregation)和依赖反转(Dependency Inversion)五部分,过多的使用继承关系来获取已有的父类功能和属性,不利于代码的可读性和可维护性,应该优先使用接口抽象和组合关系表达功能的组合。
错误示例:
public class PayrollCalcTaskRunVO extends HRBaseVO {
//.... attributes
}
等价示例:
public class PayrollCalcTaskRunVO {
@JsonUnwrapped
private HRBaseVO baseInfo;
//.... attributes
}
**说明:在Mybatis等ORM框架中可以使用合适的配置写法将结果映射成复杂对象,如Mybatis中的accociation配置。在API中可以通过 @JsonUnwrapped 等注解将属性对象的内容展开到父级json块,达到与继承相同的JSON格式。**
#### 4.4 减少switch语句的使用
switch是早期JVM提供的一个关键字,《Clean Code》中这样描述switch语句"写出只做一件事的switch语句也很难,switch天生就要做N件事"。switch语句本身很复杂,也不容易理解,一般时候应该避免/减少使用switch-case关键字的使用。
错误示例:
switch (operationType) {
case "R" : // 授权
addPermissions(rolePlanEntity, personInfoEntity.getGlobalUserId(), billDispatchRolePlanEntity);
break;
case "D" : // 延期
break;
case "C" : // 取消
deleteRolePerson(personInfoEntity.getGlobalUserId(), billDispatchRolePlanEntity);
break;
default :
log.error("IAuthServiceImpl operationType is null");
break;
}
等价示例:
Map
actionMap.put(“R”, () -> addPermissions(rolePlanEntity, personInfoEntity.getGlobalUserId(), billDispatchRolePlanEntity));
actionMap.put(“D”, () -> {});
actionMap.put(“C”, () -> deleteRolePerson(personInfoEntity.getGlobalUserId(), billDispatchRolePlanEntity));
Runnable defaultAction = () -> log.error(“IAuthServiceImpl operationType is null”);
actionMap.getOrDefault(operationType, defaultAction).run();
if-else嵌套时,会让代码的分指数显著增加,一般需要对一些代码块进行分割,并尽量共用重复逻辑部分。某些连续的 if-else 分支写法其实是类似于switch的表达,同样可以使用Map进行简化。
错误示例:
// 此处明显为固定结构,应该使用相应的类型进行描述,而不是使用Map。且变量名mp也明显缺乏含义
Map
if(“1”.equals(search_type)){
mp.put(“rehiredatebegin”, search_param);
mp.put(“rehiredateend”, search_param1);
mp.put(“search_type”, search_type);
} else if (“2”.equals(search_type)){
mp.put(“oldnumber”, search_param);
mp.put(“search_type”, search_type);
} else if(“all”.equals(search_type)){
mp.put(“search_type”, search_type);
} else {
return null;
}
mp.put(“pageInfo”,“true”);
mp.put(“pageSize”,“3000”);//分页数
mp.put(“curPage”,“1”);//当前页
等价示例:
// 将searchType、pageInfo、pageSize、curPage等固定内容放在构造函数中统一初始化
RequestParam requestParam = new RequestParam(search_type);
Map
typeActionMap.put(“1”, () -> injectParamAsDate(requestParam, search_param, search_param1));
typeActionMap.put(“2”, () -> injectParamAsNumber(requestParam, search_param));
typeActionMap.put(“all”, () -> {});
// 为重构需要,将退出语句更换为运行时异常结束
Runnable defaultAction = () -> { throw new InvalidSearchTypeException(“search type is not valid”); };
typeActionMap.getOrDefault(search_type, defaultAction).run();
返回 boolean类型的断言方法,一般只用专注与断言条件极其组合,而不需要太多分支控制,分支控制中的条件可以直接作为boolean的结果返回,让代码看起来更加简洁内聚
错误示例:
public boolean isContainsSpecilValue(String currentDeptCode, String path) {
String configValues = getRegistryValuesByPath(path);
if (!StringUtils.isEmpty(configValues)) {
String[] values = configValues.split(“,”);
if (values != null && values.length > 0) {
List list = deptQueryDao.findChildrensDeptsByCodes(Arrays.asList(values));
if (!CollectionUtils.isEmpty(list) && list.contains(currentDeptCode)) {
// 当前部门在此部门体系中,则允许异地纳税
return true;
} else {
// 不允许异地纳税
return false;
}
}
} else {
log.info(“指定的paht=” + path + “的值未配置值!”);
return false;
}
return true;
}
等价示例:
public boolean isContainsSpecilValue(String currentDeptCode, String path) {
String configValues = getRegistryValuesByPath(path);
return StringUtils.isNotEmpty(configValues) && deptQueryDao.findChildrensDeptsByCodes(newArrayList(configValues.split(“,”))).contains(currentDeptCode);
}
错误示例
final Map subMap = parserToMap(array[i]); // 由于没有指定泛型类型,代码可读性变差,后续使用时通常需要一些强制转换
对比示例
final Map
// 实在不知道类型可以使用T或者?代替
Map
错误示例
if (!save) {
throw new SystemException(“保存业务属性配置失败!”);
}
对比示例
if(newlyResult>0) {
// 该异常中获取message时,会根据code查询 i18n 配置获取
throw new BusinessI18nException(AccumulatorCalcExceptionDesc.ACC_EXISTS_NEWLY_RESULT);
}
错误示例
logger.info("success: " + dto); //不符合使用习惯
logger.error(“failed: {}”, exception); // 不会打印异常栈
logger.error(“failed: {}”, exception.getMessage()) // 仅打印异常信息
对比示例
logger.info("success : {}" , dto);
log.error("failed: ", exception); //该接口会打印异常栈
### 5. 减少重复
#### 5.1 禁止脱离业务实际需要堆砌CRUD,禁止开发预留、测试、无用的API
说明:开发人员开始编写代码时,需要有清晰具象化的业务需求和边界,而不能基于模棱两可的诉求进行开发。开发阶段对功能进行测试,验证时,应优先使用单元测试,通过编写符合业务诉求的单元测试(开发者测试)来验证和确保功能实现的正确性,而不是在产品代码中留下一些测试、无用的代码。虽然大部分模型最终都会需要CRUD等接口,但是按Story开发功能需求时,也应该首先聚焦Story包含的实际功能和边界,而不是站在开发的角度无脑添加CRUD四个接口,如此做容易留下一些实际上不会用到的功能和接口。
#### 5.2 减少重复代码,出现重复时及时重构
代码中明显存在重复的部分,应该及时重构,提取公共部分进行共用。框架性的公共代码考虑使用AOP等方式通过切面和注解统一处理。
错误示例(在每个请求之前需要初始化苍穹平台的请求上下文)
public SwitchPolicy listPolicies(RequestParam requestParam) {
initHcmRequestContext();
//... listLogic
}
public SwitchPolicy createOrUpdate(SwitchPolicy switchPolicy) {
initHcmRequestContext();
//... createOrUpdateLogic
}
private void initHcmRequestContext() {
RequestContext ctx = RequestContext.getOrCreate();
if (Objects.isNull(ctx.getAccountId())) {
ctx.setAccountId(System.getProperty("requestContext.accountId"));
}
if (Objects.isNull(ctx.getTenantId())) {
ctx.setTenantId(System.getProperty("requestContext.tenantId"));
}
}
等价示例:
// 定义为切面,通过@HcmContext注解进行标注
@Slf4j
@Component
@Aspect
public class HcmContextAspect {
@Around("@annotation(HcmContext)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
initHcmRequestContext();
log.info("Initialized HCM context ... ");
return joinPoint.proceed();
}
private void initHcmRequestContext() {
// init logic
}
}
#### 5.3 不同分支中存在较多相同逻辑时,分支只负责差异部分,相同的部分不重复书写。
避免在不同分支中出现太多重复逻辑,分支专注于差异的部分,公共部分抽取方法。
错误示例
if (privateBusiness.get(elementTypeCode) == null) {
Set privateBusinessSet = new HashSet<>();
for (String string : strings) {
if (string != null && !string.trim().equals("")) {
privateBusinessSet.add(string);
}
}
privateBusiness.put(elementTypeCode, privateBusinessSet);
} else {
Set privateBusinessSet = privateBusiness.get(elementTypeCode);
for (String string : strings) {
if (string != null && !string.trim().equals("")) {
privateBusinessSet.add(string);
}
}
privateBusiness.put(elementTypeCode, privateBusinessSet);
}
等价示例:
Set privateBusinessSet = Optional.ofNullable(privateBusiness.get(elementTypeCode)).orElse(new HashSet<>());
for (String string : strings) {
if (string != null && !string.trim().equals("")) {
privateBusinessSet.add(string);
}
}
privateBusiness.put(elementTypeCode, privateBusinessSet);
### 6. 推荐工具和表现力较强的API
#### 6.1 使用stream接口替换常用的循环操作
错误示范:
boolean isAllow = false;
if (StringUtil.isNotNull(cities)) {
String[] cityArr = cities.split(",");
for (String city : cityArr) {
city = city.trim();
if (city.equals(tlasApplicationVO.getToResidentLocation()+"")) {
isAllow = true;
break;
}
}
}
等价示例:
boolean isAllow = StringUtil.isNotNull(cities) && Stream.of(cities.split(","))
.anyMatch(city -> city.trim().equals(tlasApplicationVO.getToResidentLocation() + ""));
Stream常用操作附录:
| API | 作用| 使用场景| 案例代码|
|--|--|--|:--|
| map | 映射操作,将集合中每个元素转换为其他格式的元素 | 按照用户列表获取每个用户的常驻地 | List userLocations = userList.stream().map(User::getLocation).collect(toList()) |
| filter | 按照条件筛选集合中满足条件的元素 |筛选有效账号 |List enabledUsers = userList.stream().filter(user -> Objects.equals("Y", user.getEnableFlag())).collect(toList()); |
| peek |在循环中做额外动作(如打日志),不改变输入输出 | 打印stream中的处理日志 | userList.stream().peek(user -> logUserName(user)).filter(user -> isValid(user)).collect(toList()) |
| reduce |聚合集合元素/特征,如求和 |计算用户列表的工资总额 | Integer totalSalary = userList.stream().map(user -> user.getSalary()).reduce(0, Integer::sum) |
| allMatch | 判断集合中是否每个元素都满足某条件 | 判断是否都是有效用户 | Boolean isAllValid = userList.stream().allMatch(user -> isValid(user)); |
| anyMatch |判断集合中是否有至少一个元素满足条件 |判断是否包含异常用户 | Boolean existAbnormalUser = userList.stream().anyMatch(user -> isAbnormal(user)); |
| Collectors.groupBy|按元素特征分类 |按用户类型进行分类 | Map> groupedUser = userList.stream().collect(groupingBy(User::getType)); |
#### 6.2 guava 和各种 Utils 工具包
guava是google提供的开发工具包,在Java8提供Lambda写法之前就已经存在,其提供的多种函数式编程接口非常流行。在Java8发布之后,许多原有的功能逐渐内置到了JVM中,但仍然存在一些方便的工具可供使用。以其集合包(还有数学等其他辅助工具包)中的一些常用接口举例如下:
| 接口名 | 功能说明 | 举例 |
|--|--| --|
| Lists.newArrayList |构造一个List对象,可以接受数组,其他List,或者可变长的Item列表 | List personList = newArrayList(personOne, personTwo); |
|Lists.partition | 将List按照指定最大长度拆分为一些小的List | List> subLists = partition(personList, 100); |
|Sets.intersection / union |取两个集合的交集 / 并集 |Set answer = Sets.intersection(set1, set2); |
除guava外,还有一些Apache提供的Utils工具包,对于一些常见模式的问题提供了接口封装,如StringUtil,Json,XML等相关的常用工具等。
### 7 使用框架能力解决通用问题
#### 7.1 对象转换工具 Mapstruct & spring BeanUtils
在不同的层次之间,经常需要进行类型转换,与其手写转换关系,不如使用一些常用的工具,如MapStruct,spring BeanUtils等。MapStruct会根据对象结构的字段名称,在编译阶段生成转换代码,类型安全,且自动生成空保护等语句,通过声明式配置指定转换关系。spring BeanUtils基于反射机制进行对象属性映射,也可以省略一部分手工编写的类型转换代码,但是需要注意copyProperties自身的特性,如需要类型匹配,以及不会忽略null属性等。
错误示例:
UserInfoBean userInfoBean = new UserInfoBean(null);
userInfoBean.setUid(user.getUid());
userInfoBean.setEmployeeNumber(user.getEmployeeNumber());
userInfoBean.setEmail(user.getEmail());
userInfoBean.setEmployeeType(user.getEmployeeType());
userInfoBean.setCn(user.getCn());
等价示例:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserInfoBean toBean(User user)
}
#### 7.2 使用框架Validator进行参数校验,减少手动实现
请求参数的非空性,有效性,长度范围,取值范围校验等属于常用的框架级校验,一般应该借助框架工具来控制,而不需要手动编写代码逐个字段进行手动校验
错误示例:
public Boolean submitVisaToDo(SubmitVisaAndSalaryTaxDto submitVisaAndSalaryTaxDto) {
VisaInfoDto visaInfoDto = submitVisaAndSalaryTaxDto.getVisaInfoDto();
// 签证数据基础非空校验
if (visaInfoDto.getCategoryCode() == null) {
throw new ResultException(I18nConstant.VISA_CATEGORY_NULL);
}
if (StringUtil.isNullOrEmpty(visaInfoDto.getTypeCode())) {
throw new ResultException(I18nConstant.VISA_TYPE_NULL);
}
if (StringUtil.isNullOrEmpty(visaInfoDto.getComplianceApproval())) {
throw new ResultException(I18nConstant.SALARY_TAX_COMPLANCE_APPROVAL_NULL);
}
if (!StringUtil.isNullOrEmpty(visaInfoDto.getDescription()) && visaInfoDto.getDescription().length() > ContractPlanEnum.ONE_THOUSAND.getCode()) {
throw new ResultException(I18nConstant.SALARY_TAX_REMIND_BEYOND_ONE_THOUSAND);
}
//... business logic
}
等价示例:
public class VisaInfoDto {
/**
* 签证大类
*/
@ApiModelProperty("签证大类id")
@NotBlank(message = I18nConstant.VISA_CATEGORY_NULL)
private Long categoryCode;
/**
* 签证类型编码
*/
@ApiModelProperty("签证类型编码")
@NotBlank(message = I18nConstant.VISA_TYPE_NULL)
private String typeCode;
/**
* 是否涉及合格审批
*/
@ApiModelProperty("是否涉及合格审批")
@NotBlank(message = I18nConstant.SALARY_TAX_COMPLANCE_APPROVAL_NULL)
private String complianceApproval;
/**
* 重要提示
*/
@ApiModelProperty("重要提示")
@Size(max = 1000, message = I18nConstant.SALARY_TAX_REMIND_BEYOND_ONE_THOUSAND)
private String description;
}
#### 7.3 使用統一异常处理器,取代每个API的单独手工处理
后台服务在发生异常时,通常需要对异常信息进行一定的处理和包装,结构上可与正常相应存在差异。在Spring框架中提供了统一的异常处理机制(@RestControllerAdvice),按照异常类型和code进行统一修饰。无需每个API单独处理一遍。
错误示例:
@Override
public BasicResponse export(PayElementLabelPageDto queryDTO) {
try {
localExcelExportAssistant.submitExportTask("payrollCalcService.PayElementLabel", queryDTO);
return BasicResponse.ok();
} catch (ApplicationException e) {
log.error("export payElementLabel error:", e);
return BasicResponse.error();
}
}
等价示例:
@Slf4j
@Provider
@Named("applicationExceptionHandler")
public class ApplicationExceptionHandler implements ExceptionMapper {
@Override
public Response toResponse(ApplicationException exception) {
log.error("A ApplicationException occurred during the request process:", exception);
return Response.status(Response.Status.BAD_REQUEST)
.type("application/json;charset=UTF-8")
.entity(BasicResponse.error())
.build();
}
}
#### 7.4 使用Builder模式构造对象
builder模式提供了一种可联连续设置属性的构造对象方式,可以较方便的将属性值构造成对象,且避免了临时变量的多次出现。
错误示例
PageDTO pageDTO = new PageDTO();
pageDTO.setPageNo(curPage);
pageDTO.setPageSize(pageSize);
等价示例:
PageDTO pageDto = PageDTO.builder.pageNo(curPage).pageSize(pageSize).build();
注意,使用Lombok注解@Builder时,会生成全参构造器覆盖默认的无参构造器,如果需要保留无参构造器,应叠加@NoArgsConstructor注解一起使用
Lombok相关的注解,可以在编译期自动生成一下常用的模板代码,如构造函数,getter/setter等,能有效减少模板代码。
错误示例:
public class PageDTO implements Serializable {
private static final long serialVersionUID = 1211673654467855785L;
private Integer pageNo = 1;
private Integer pageSize = 15;
public PageDTO() {
}
public Integer getPageNo() {
return this.pageNo;
}
public void setPageNo(Integer pageNoTemp) {
this.pageNo = pageNoTemp;
}
public Integer getPageSize() {
return this.pageSize;
}
public void setPageSize(Integer pageSizeTemp) {
this.pageSize = pageSizeTemp;
}
}
等价示例:
@Getter
@Setter
public class PageDTO implements Serializable {
private static final long serialVersionUID = 1211673654467855785L;
private Integer pageNo = 1;
private Integer pageSize = 15;
}
说明:添加@Data注解时,除了生成getter/setter外,还会生成包含所有属性的equals和hashCode方法,在对象属性较多(接近1000个左右)时会无法编译。一般而言仅需要Getter/Setter而不需要进行对象比较时,可以只添加@Getter/@Setter注解,而不是直接使用 @Data。
说明:Intellij是一个非常智能的IDE,在源代码中,会给出诸多优化建议和提示,如使用灰色文字表示没有被用到的变量/参数,使用黄色背景给出一些优化建议等。鼠标停留在提示的部分,IDE会给出相应的说明,使用Alt+Enter会给出对应的修改建议(等价于鼠标点击浮出的灯泡按钮)。IDE提示的范围非常广,不一一展开称述,编写代码时务必参考IDE进行一些必要的优化,举例如下:
错误示例:
// 集合类已经自己扩充了forEach接口,无需转换为stream便可遍历
resultDtos.stream().forEach
// IDE 提示可以使用Lambda写法,省略临时形参
batchPersonList.forEach(batchList -> save(batchList));
// IDE 提示可以使用anyMatch写法
public static boolean isValidState(int flag) {
for (PaymentMethodEnum value : PaymentMethodEnum.values()) {
if (flag == value.getFlag()) {
return true;
}
}
return false;
}
说明:Intellij中还包含许多功能强大的插件,代码检查、统计、生成等插件等,不展开具体说明。
说明:用于测试的代码,直接放到测试类中写成单元测试用例,不要遗留在产品代码中,弃用的代码直接删除,不要以注释/标注的方式遗留在代码中
错误示例:
//本地测试放开
// public void doGet(HttpServletRequest req, HttpServletResponse resp) {
// logger.info(“----------local—test—start------------”);
// AutoRetentionDataService autoRetentionDataService = SpringContextUtil.getBean(AutoRetentionDataService.class);
// autoRetentionDataService.asyncExecute();
// logger.info(“----------local—test—end--------------”);
// }
存在于代码中的注释,初衷是对代码进行说明,但是一般的开发习惯中,对于编译错误,运行错误更为关注,而对于说明性的文档通常难以时时确保其正确和及时更新,注释极易腐化。因此应该将精力花在编写可执行,可校验的单元测试代码上,而非撰写过多说明性注释。
错误示例:
//1、法人实体为发薪公司
// 1.1、查看法人实体Legal Entity Code(companyCode)在配置列表里
// 1.2、满足1 则查看COA对应的组织属于国内
// 1.3、则Legal Entity Code(companyCode)作为发薪公司
if(isHasLegalEntity && isHasCoa){
result = companyCode;
//2、coa为发薪公司
// 2.1、查看法人实体Legal Entity Code不在配置列表里
// 2.2、则查看COA,COA作为发薪公司
}else if(!isHasLegalEntity){
result = orgCoa;
//3、不识别发薪公司
// 3.1、查看法人实体Legal Entity Code(companyCode)在配置列表里
// 3.2、COA对应的组织不属于国内
}else{
}
等价示例:
@Test
public void should_use_company_code_as_payment_company_when_coa_is_chinese_org() {
// 场景先关的上下文准备
assertEquals(“test_company_code”, extractedMethod(someCondition));
}
@Test
public void should_use_coa_as_payment_company_when_legal_entity_is_not_configured() {
// 场景先关的上下文准备
assertEquals(“test_coa”, extractedMethod(someCondition));
}
@Test
public void should_use_empty_payment_company_when_legal_entity_is_not_configured_and_coa_is_not_chinese() {
// 场景先关的上下文准备
assertNull(extractedMethod(someCondition));
}
测试代码应该和产品代码严格区分,禁止在产品代码中添加静态main方法用于测试。使用产品代码中的main方法测试,不容易自动化,也存在污染产品代码的风险,需要单元测试时应该将测试用例代码放在test目录下
错误示例:
public static void main() {
LocalDateTime start = LocalDateTime.of(2022,7,01,12,20,21);
LocalDateTime end = LocalDateTime.of(2022,8,29,12,20,21);
Date date1 = LocalDateTimeToDate(start);
Date date2 = LocalDateTimeToDate(end);
System.out.println(“工作日:” + getTotalWorkdaysNum(date1, date2));
}