软件帮助及JAVA开发规范

https://www.cnblogs.com/xpybsh/p/12703269.html

1 目的
本规范的目的是基于开发过程中的各种踩坑经验,面对同一需求多种实现选择的情况,提炼出一些大多数开发人员都能避免踩坑的实现方案,故此命名为“填坑规范”。

2 概述
Java是一门易学难精的语言,坑多水深的环境,加上开发人员参差不齐的水平,导致各种缺陷代码不胜枚举。这些缺陷代码一部分在开发阶段暴露出来,一定程度上影响了开发效率,还有部分没暴露出来的缺陷,随着不同版本的迭代,最终成为各种现场问题的导火索。本规范基于新构架的基线项目开发经验编写。涉及的规范除了Java编码外,还包含一些打包启动、代码提交、协作开发等方面的内容。

3 规范
3.1 Java编码
3.1.1 【规范】只有final类型且引用不可修改对象的变量能直接申明为public类型
【反例】基线开发中发现常量类中创建了大量可修改对象,并对外提供:

public static final Map<Integer, String> BASIC_COLOR_DICT = new HashMap<>();
static {
BASIC_COLOR_DICT.put(1, “黄色”);
BASIC_COLOR_DICT.put(2, “蓝色”);
BASIC_COLOR_DICT.put(3, “绿色”);
}
在代码审核时,发现了如下调用代码:

public Map<Integer, String> getExtendColorDict(Integer extendKey) {
Map<Integer, String> colorMap = BASIC_COLOR_DICT;
if(extendKey == null) {
return colorMap;
}
switch (extendKey) {
case 4:
colorMap.put(extendKey, “紫色”);
break;
case 5:
colorMap.put(extendKey, “青色”);
break;
default:
break;
}
return colorMap;
}
测试代码如下:

@PostConstruct
public void testObjectProtect() {
logger.info(“对象保护测试:”);
logger.info(getExtendColorDict(4).toString());
logger.info(getExtendColorDict(5).toString());
}
执行结果:

2019-06-05 10:09:34.663 INFO 10904 — [ main] o.b.s.codestandard.ObjectProtect : 对象保护测试:

2019-06-05 10:09:34.663 INFO 10904 — [ main] o.b.s.codestandard.ObjectProtect : {1=黄色, 2=蓝色, 3=绿色, 4=紫色}
2019-06-05 10:09:34.663 INFO 10904 — [ main] o.b.s.codestandard.ObjectProtect : {1=黄色, 2=蓝色, 3=绿色, 4=紫色, 5=青色}

结论:

可修改对象直接对外提供是不安全的,无法预计外部会对这个对象进行何种操作。

解决方案:

把目标对象包装成不可修改的对象;
提供目标对象的副本。
【正例】上述代码修正:

public static final Map<Integer, String> BASIC_COLOR_DICT;
static {
Map<Integer, String> CORE_BASIC_COLOR_DICT = new HashMap<>();
CORE_BASIC_COLOR_DICT.put(1, “黄色”);
CORE_BASIC_COLOR_DICT.put(2, “蓝色”);
CORE_BASIC_COLOR_DICT.put(3, “绿色”);
BASIC_COLOR_DICT = Collections.unmodifiableMap(CORE_BASIC_COLOR_DICT);
}
BASIC_COLOR_DICT此时引用的对象已经是包装后的不可变对象,在调用代码不变的情况下将会抛出异常:java.lang.UnsupportedOperationException
在这种前提下,调用方不得不在方法内重新创建一个对象,就避免了缺陷代码的产生:
public Map<Integer, String> getExtendColorDict(Integer extendKey) {
Map<Integer, String> colorMap = new HashMap<>(BASIC_COLOR_DICT);
if(extendKey == null) {
return colorMap;
}
switch (extendKey) {
case 4:
colorMap.put(extendKey, “紫色”);
break;
case 5:
colorMap.put(extendKey, “青色”);
break;
default:
break;
}
return colorMap;
}
重新执行测试代码,结果如下:

2019-06-05 10:39:36.012 INFO 3168 — [ main] o.b.s.codestandard.ObjectProtect : 对象保护测试:
2019-06-05 10:39:36.012 INFO 3168 — [ main] o.b.s.codestandard.ObjectProtect : {1=黄色, 2=蓝色, 3=绿色, 4=紫色}
2019-06-05 10:39:36.012 INFO 3168 — [ main] o.b.s.codestandard.ObjectProtect : {1=黄色, 2=蓝色, 3=绿色, 5=青色}

除了上面举例的Map实例外,常见的List、Set等实例都可以使用 Collections.unmodifiableXXX 包装成不可变对象。

对于数组、Date等类型的实例,建议使用clone方法对外提供副本:

【反例】:

public static final String[] COLOR_ARRAY = {“red”, “blue”, “green”};

【正例】:

private static final String[] COLOR_ARRAY = {“red”, “blue”, “green”};

public static String[] getColorArray() {
return COLOR_ARRAY.clone();
}

3.1.2 【规范】枚举类中不提供对外的set方法,使枚举类的实例作为固定的全局实例
以规范3.1.1为基准,枚举类的实例作为全局实例,不允许更改其内部属性
3.1.3 【建议】慎用for循环,尽量使用foreach循环替代,尤其是在使用多个循环嵌套的情况下
慎用for循环,尽量使用foreach循环替代,主要有以下原因:

   1.  一般来说,未继承 RandomAccess 接口(仅用于标记)的类 使用foreach循环的效率 大幅高于 for循环,而继承 RandomAccess 接口的类 foreach循环的效率 只略低于 for循环;

   2. 多层嵌套时,里层的for循环变量容易混用,导致功能缺陷。

【反例】: easypoi 3.1.0中,有一段缺陷代码如下:

public void setColumnHidden(List excelParams, Sheet sheet){
int index = 0;
for (int i = 0; i < excelParams.size(); i++) {
if (excelParams.get(i).getList() != null) {
List list = excelParams.get(i).getList();
for (int j = 0; j < list.size(); j++) {
sheet.setColumnHidden(index,list.get(i).isColumnHidden());
index++;
}
} else {
sheet.setColumnHidden(index,excelParams.get(i).isColumnHidden());;
index++;
}
}
}
此代码对基线的导出功能造成了一定影响,最后不得不以覆盖源码的方式进行处理,而覆盖源码又导致了jar包冲突,对开发效率造成了一定影响。归根结底,还是由于作者在编写这段代码时过于随意、

使用多层for循环嵌套,加上测试不够全面造成。上述代码使用foreach循环替换后,出现类似问题的概率就很低了。

【正例】: 基线覆盖easypoi代码:

public void setColumnHidden(List excelParams, Sheet sheet){
int index = 0;
for (ExcelExportEntity excelParam : excelParams) {
if (excelParam.getList() != null) {
List list = excelParam.getList();
for (ExcelExportEntity aList : list) {
sheet.setColumnHidden(index, aList.isColumnHidden());
index++;
}
} else {
sheet.setColumnHidden(index, excelParam.isColumnHidden());
index++;
}
}
}

3.1.4 【建议】依赖注入优先考虑构造器注入,慎用字段注入
依赖注入时使用字段注入,由于缺少语法约束,可能造成以下情况:

    1.对于没有熟悉IOC模式开发的新同事来说,经常会直接new对象,造成依赖缺失; 

    2.对于确实需要new对象的情况,没法识别哪些对象是必要注入,哪些对象可以选择性注入; 

    3.为属性赋值或者重新注入时只能依赖于反射,很难写出优雅的代码;

建议通过构造器注入和set方法注入联合使用的方式替代字段注入,必须注入的对象使用构造器注入,并且可以把变量申明为final 类型, 反之如果是非必要注入或者可替换的对象,则可以使用set方法注入。 使用构造器注入, 可以有效避免直接调用无参构造器造成的依赖缺失, 同时与IOC容器解耦,在单例对象不能满足业务需求的情况下,支持调用有参构造器创建对象,而不必通过反射为对象赋值。

【反例】有同事在定时任务中未使用spring容器中的单例对象,直接new了一个,报空指针后一直认为是注入失败,各种检查配置,在排查问题上花费了大量时间,模拟代码如下:

@Component
public class DependencyInjection {

@Autowired
private UserService userService;

@Scheduled(cron = "0/30 * * * * ?")
public void execute() {
    DependencyInjection dependencyInjection = new DependencyInjection();
    dependencyInjection.getAllUser();
}

public DependencyInjection() {
}

public List<User> getAllUser() {
    return userService.getAllUser();
}

}
把字段注入改为构造器注入后,在开发工具进行代码检查时就定位到了问题:
在这里插入图片描述

上面提示变量可能未初始化,证明类中还有其他构造器,找到下面的无参构造器后,顺藤摸瓜,马上就发现了new DependencyInjection 的相关代码。

3.1.5 【规范】使用构造器注入时,无论是否只有一个构造器都需加上@Autowired
使用构造器注入时,无论是否只有一个构造器都需加上@Autowired,防止后续添加构造器时IOC容器无法精确定位到具体的构造器

3.1.6 【规范】当一个Bean的创建依赖于另一个Bean的创建时,必须显式指定依赖关系
当一个Bean的创建依赖于另一个Bean的创建时,必须显式指定依赖关系,如:@DependsOn(“appContext”),容器默认的加载顺序非常容易发生变化,
导致启动报错等问题

3.1.7 【规范】依赖注入时,申明类型禁止使用实现类来替代接口
依赖注入的申明类型不能使用实现类来替代接口的原因主要有:
1. 依赖注入最大的好处就是利用java的多态特性减少耦合,申明类型使用实现类将造成使用层和实现层无法解耦;

       2. 申明类型为实现类时,基于接口的动态代理将无法使用,否则会造成启动失败。

【反例】Controller与Impl形成耦合关系,无法进行拆分,且不支持使用jdk动态代理对BreakBanRecordService进行功能增强:

public class BreakBanRecordController extends BaseController {
@Autowired
private BreakBanRecordServiceImpl breakBanRecordServiceImpl;
}

@Service
public class BreakBanRecordServiceImpl extends BaseService implements BreakBanRecordService {
}
【正例】:

public class BreakBanRecordController extends BaseController {
@Autowired
private BreakBanRecordService breakBanRecordService;
}

3.1.8 【规范】输出日志时,拼接字符串使用占位符
【反例】不判断日志是否可打印的前提下,使用字符串拼接或字符串格式化:

logger.info(String.format(“hello, my name is %s, my age is %s”, name, age));
改进(先判断是否可打印,不可打印时省下了字符串格式化的时间):

if(logger.isInfoEnabled()) {
logger.info(String.format(“hello, my name is %s, my age is %s”, name, age));
}
【正例】使用占位符来避免不可打印时字符串格式化的消耗(类似上面改进后的效果):

logger.info(“hello, my name is {}, my age is {}”, name, age);

3.2 打包启动
3.2.1 【规范】开发环境与生产环境下的配置隔离
大多数项目中都有多环境配置,比如开发环境下的配置放在application-dev.properties,生产环境下的配置放在application.properties,通过spring.profiles.active这个属性设置运行环境。很多项目直接在application.properties 中添加配置 spring.profiles.active=dev 来指定当前运行环境,这种方式无法完全将开发环境与生产环境下的配置隔离开,因为一旦有人不小心提交 spring.profiles.active=dev 这个配置,运行环境就变掉了。建议在开发工具中指定运行环境,防止误提交,如:

在这里插入图片描述

3.2.2 【规范】打包时排除开发环境下的配置
开发环境下的配置在打包时应该排除, 如cas-client-dev.properties、config.properties、application-dev.yaml等,防止生产环境下读到这些文件,影响功能的正常运行。

3.2.3 【规范】远程配置不能直接添加在源代码
由于远程调试等需要,有些项目直接把远程配置添加在源代码中,如:

_service
J S V C − D _JSVC -D JSVCD{_ServerName} -home $JAVA_HOME -cwd ${_HOME_DIR}/config > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5041 -Djava.net.preferIPv4Stack=true XXX
这样在开发阶段确实会比较方便,但是这段配置在提测或发布打包时容易忘记删除,从而给产品造成安全问题。在需要远程调试的情况下,可以先部署项目,部署好以后在服务器修改启动脚本,再重启项目,就能避免上述问题。

3.2.4 【建议】jar包加载方式生产环境与开发环境保持一致
目前新构架下脚手架提供的启动脚本中,指定jar包的方式是 /lib/* ,而开发环境下一般是从启动jar包开始查找依赖关系。当出现jar包冲突,或者代码中需要覆盖源码的类时, 容易出现在开发环境正常运行,生产环境运行出现问题的情况。 这种情况下可以定期分析jar包冲突,或者出问题时改变指定jar的顺序来解决,但更合理的方法是统一运行方式。建议都把jar包的依赖关系打在start的jar包中,运行脚本指定/lib/start-1.0.0-SNAPSHOT.jar 来替代 /lib/*。

以jsvc 启动方式为例:

_service 文件中

_Classpath= C L A S S P A T H : " {CLASSPATH}:" CLASSPATH:"{_HOME_DIR}/config":" H O M E D I R / l i b / ∗ " : " {_HOME_DIR}/lib/* ":" HOMEDIR/lib/":"{_HOME_DIR}/…/…/conf" 改为

_Classpath= C L A S S P A T H : " {CLASSPATH}:" CLASSPATH:"{_HOME_DIR}/config":" H O M E D I R / l i b / s t a r t − 1.0.0 − S N A P S H O T . j a r " : " {_HOME_DIR}/lib/start-1.0.0-SNAPSHOT.jar":" HOMEDIR/lib/start1.0.0SNAPSHOT.jar":"{_HOME_DIR}/…/…/conf"

start 模块的pom文件中,maven-jar-plugin 插件添加配置(把依赖关系按顺序添加在start的jar包描述文件中:MANIFEST.MF):

org.apache.maven.plugins maven-jar-plugin true

3.3 代码提交
3.3.1 【规范】开发无关文件应revert掉或设置忽略提交
开发无关文件如Test.java, 应revert掉(不用删除文件,只需从提交列表中删除即可),防止误提交,较通用的开发无关文件应在git或svn中设置排除,如在.gitignore 文件中把 //.idea、//*.iml、/**/.log 、target、logs 等常见文件设置忽略。

3.4 协作开发
3.4.1 【建议】不直接删除正在使用的类或者方法
举个例子,当通用代码中的function1 设计不是很合理时, 这个时候设计出了替代方法function2, 如果直接删除function1, 把所有使用到的地方改为function2, 当使用的地方比较多时,很容易跟其他同事产生代码冲突。同时,由于其他同事不了解这块改动,也可能引发其他的问题。建议的做法是把function1标注为过期, 在开发到一定阶段时,再推动大家一起把所有过期的方法都替换掉。这时候发现function1没有再被使用了,再进行删除。在java中可使用@Deprecated对弃用的类或者方法进行标注。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值