JavaWeb之谈论项目编码规范_Java版

1. 关于DDD项目结构约定

1.1 项目结构使用DDD整洁架构进行分包

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接口定义,并依据接口定义提供实现

应用层依赖领域层处理业务逻辑

应用层可以使用部分基础设施层工具、类型

领域层不反向依赖应用层和接口层。可以依赖部分基础设施层工具和类型

2. 禁止大面积拷贝

2.1 禁止拷贝反编译生成的代码

说明: 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 = newHashMap();
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();

4.5 避免过多的if-else嵌套,switch式的if-else使用map进行简化

if-else嵌套时,会让代码的分指数显著增加,一般需要对一些代码块进行分割,并尽量共用重复逻辑部分。某些连续的 if-else 分支写法其实是类似于switch的表达,同样可以使用Map进行简化。

错误示例:

// 此处明显为固定结构,应该使用相应的类型进行描述,而不是使用Map。且变量名mp也明显缺乏含义
Map mp = new HashMap();
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 = newHashMap();
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();

4.6 逻辑关系简明化,避免过多 return true/ false的指令

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

4.7 使用泛型时需要显式指定类型,避免代码含糊不清

错误示例

final Map subMap = parserToMap(array[i]); // 由于没有指定泛型类型,代码可读性变差,后续使用时通常需要一些强制转换

对比示例

final Map subMap = parserToMap(array[i]);
// 实在不知道类型可以使用T或者?代替
Map subMap = parserToMap(array[i]);

4.8 抛出业务异常时,异常信息必须考虑多语言,禁止中文硬编码

错误示例

if (!save) {
throw new SystemException(“保存业务属性配置失败!”);
}

对比示例

if(newlyResult>0) {
// 该异常中获取message时,会根据code查询 i18n 配置获取
throw new BusinessI18nException(AccumulatorCalcExceptionDesc.ACC_EXISTS_NEWLY_RESULT);
}

4.9 按照正确的格式打印日志

错误示例

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注解一起使用

7.5 使用Lombok注解生成模板代码

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

7.6 使用和参考IDE提示进行必要的简化和重构

说明: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中还包含许多功能强大的插件,代码检查、统计、生成等插件等,不展开具体说明。

8 注释的使用

8.1 废弃的代码,直接删除,不要通过注释标记废弃

说明:用于测试的代码,直接放到测试类中写成单元测试用例,不要遗留在产品代码中,弃用的代码直接删除,不要以注释/标注的方式遗留在代码中

错误示例:

//本地测试放开
// 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--------------”);
// }

8.2 减少过多不必要的注释,使用单元测试用例描述复杂代码的行为

存在于代码中的注释,初衷是对代码进行说明,但是一般的开发习惯中,对于编译错误,运行错误更为关注,而对于说明性的文档通常难以时时确保其正确和及时更新,注释极易腐化。因此应该将精力花在编写可执行,可校验的单元测试代码上,而非撰写过多说明性注释。

错误示例:

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

8.3 禁止在产品代码中添加仅用于测试的main方法

测试代码应该和产品代码严格区分,禁止在产品代码中添加静态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));
}

你可能感兴趣的:(编程工作之项目总结,java,jvm,开发语言)