Java 开发经验分享
本文从开发规范、三方类库选择、工程结构设计和测试相关等几个方面介绍了 Java 开发的一些经验和建议。
关于 Java
引用王垠(http://www.yinwang.org/)的文章:
《给 Java 说句公道话》
- Java 超越了所有咒骂它的"动态语言"
- Java 的"继承人"没能超越它
- Java 没有特别讨厌的地方
《Kotlin 和 Checked Exception》
"如果你经过理性的分析,就会发现 Java 并不是那么的讨厌。正好相反,Java 的有些设计看起来"繁复多余", 实际上却是经过深思熟虑的决定。Java 的设计者知道有些地方可以省略,却故意把它做成多余的。不理解语言 "可用性"的人,往往盲目地以为简短就是好,多写几个字就是丑陋不优雅,其实不是那样的。"
1. 开发规范
命名规范
-
驼峰命名法: 类型名称、方法名、变量名
- 反例:
java
public static class XmlError { } private String Key; private String Code;
public static class XmlError { } private String Key; private String Code;
- 反例:
-
字母大写、单词中间加下划线: 枚举值、常量
- 反例:
java
public enum BodyType { TEXT, BINRAY, NTFS, EncryptNTFS, NTFSFDS }
public enum BodyType { TEXT, BINRAY, NTFS, EncryptNTFS, NTFSFDS }
- 反例:
-
变量名要做到见名知义,不要使用意义不明的缩写
- 反例:
java
AppException e = new AppException(……); private String scName; public void creatDomain() { } List<NameValuePair> nvps = new ArrayList<NameValuePair>(); Properties p;
AppException e = new AppException(……); private String scName; public void creatDomain() { } List<NameValuePair> nvps = new ArrayList<NameValuePair>(); Properties p;
- 反例:
-
对于单元测试:
- 测试类和被测类应该在同一个包中
- 不需要 import 被测类
- 可以直接测试 protected 和 default 访问控制符修饰的方法
- 很多测试插件、工具的默认配置
- 测试类命名:"被测类名称" + "Test"
- 测试方法命名:"test" + "被测方法名(首字母大写)"
- 反例:
java
public class PostDomainServiceTest extends PostDomainService { @Test public void TestFuncParseAndCheck() { //.... } }
public class PostDomainServiceTest extends PostDomainService { @Test public void TestFuncParseAndCheck() { //.... } }
- 测试类和被测类应该在同一个包中
格式规范
- 常见规范:
- 用四个空格缩进,而不是 TAB
- 左大括号不换行
- 二元操作符左右两边要加空格
- 参数分割的逗号后面要加空格
- 团队统一
注释规范
-
单行注释
java//这个是单行注释 Record result = doRequest(httpPost, Record.class);//这个是行末注释,不要使用这种注释
//这个是单行注释 Record result = doRequest(httpPost, Record.class);//这个是行末注释,不要使用这种注释
-
多行注释
java/* 如果注释比较长的话,可以使用多行注释 注意,不要写成 Javadoc 注释 */
/* 如果注释比较长的话,可以使用多行注释 注意,不要写成 Javadoc 注释 */
-
TODO 和 FIXME
java// TODO 这个功能还没有完成 // FIXME 这段代码需要修复
// TODO 这个功能还没有完成 // FIXME 这段代码需要修复
-
Javadoc
- 应用于:类、成员变量、成员方法
- 至少所有的非 private 方法都需要写 Javadoc 注释
- 示例:
java
/** * 创建 CNAME 记录 * @param srcDomain 源域名 * @param dstDomain 目标域名 * @return 任务 id * @throws DnsException 如果响应状态码不是 200 * @throws IOException IO 异常 */ public String createCname(String srcDomain, String dstDomain) throws DnsException, IOException { //... }
/** * 创建 CNAME 记录 * @param srcDomain 源域名 * @param dstDomain 目标域名 * @return 任务 id * @throws DnsException 如果响应状态码不是 200 * @throws IOException IO 异常 */ public String createCname(String srcDomain, String dstDomain) throws DnsException, IOException { //... }
Java 规范
- 每个 Java 新版本发行,都会发布该版本对应的语言规范和虚拟机规范。
- 示例: 字段修饰符顺序
- Java 语言规范 (
8.3.1. Field Modifiers
) 建议,但不强制,字段修饰符遵循以下顺序:Annotation public protected private static final transient volatile
- 如果出现多个(不同的)字段修饰符,习惯上(尽管不是必需的)它们应按照上述顺序出现。
- Java 语言规范 (
静态代码检查工具
- 工具: IDE 自带,PMD, CheckStyle, Sonar
- 能做什么:
- 命名约定: 检查命名是否符合命名规范
- Javadoc 注释: 检查类及方法的 Javadoc 注释
- 重复的代码: 检查重复的代码
- 资源关闭: 检查 Connect, Result, Statement 等资源使用之后是否被关闭掉
- 潜在的性能问题: 例如循环体内使用字符串拼接
- 可能的 Bugs: 检查潜在代码错误,如空
try/catch/finally/switch
语句
- 规则集: PMD 和 Sonar 等工具都提供了详细的规则库。
- 处理规则:
- 有些规则可能比较"烦人",例如要求在工具类中加上私有构造器、使用 try-with-resources 管理资源、要求降低访问控制权限等。
- 可以使用
@SuppressWarnings
注解或在工具中禁用特定规则来处理。java@SuppressWarnings("all") // 不推荐 @SuppressWarnings("unchecked") List<String> strings = (List<String>) object;
@SuppressWarnings("all") // 不推荐 @SuppressWarnings("unchecked") List<String> strings = (List<String>) object;
无用代码
-
多余的修饰符: 例如在局部变量前加
final
、在枚举的构造器加private
等。 -
无用的赋值:
- 例如给 String 类型的成员变量赋初值
null
, 给 int 类型成员变量赋初值0
。 - 在所有分支都重新赋值的情况下,给局部变量赋初值。
java
int a = 0; // 'a'的初始化是多余的 if (Math.random() > 0.5) { a = 1; } else { a = 2; }
int a = 0; // 'a'的初始化是多余的 if (Math.random() > 0.5) { a = 1; } else { a = 2; }
- 例如给 String 类型的成员变量赋初值
-
注释掉的代码: 应该删除,如果要找回来请通过版本控制。
-
测试用的 main 函数: 应该去掉,可能会影响性能。
-
非必要 toString()、显式拆装箱:
- 显式装箱
func1(a)
和拆箱func2(b)
可能会产生空指针异常。
- 显式装箱
-
关于 boolean 值: 避免冗余的布尔比较。
- 反例:
java
boolean isDedup = (bucket.getDedup() == AppConstant.DEDUPLICATE_ENABLED) ? true : false; if (SIGNED_PARAMETERS.contains(parameterName) == false) //可以简化为 return bbs != null && bbs.getBanstatus() == 1; private boolean isBan(long bid) { BucketBanStatus bbs = AppBucketBanStatusCacheManager.get(bid); if (bbs != null && bbs.getBanstatus() == 1) { return true; } return false; }
boolean isDedup = (bucket.getDedup() == AppConstant.DEDUPLICATE_ENABLED) ? true : false; if (SIGNED_PARAMETERS.contains(parameterName) == false) //可以简化为 return bbs != null && bbs.getBanstatus() == 1; private boolean isBan(long bid) { BucketBanStatus bbs = AppBucketBanStatusCacheManager.get(bid); if (bbs != null && bbs.getBanstatus() == 1) { return true; } return false; }
- 反例:
-
重复代码
==
vs .equals
- 所有非基本类型应该使用
.equals()
判断是否相等,除非目的是比较内存地址。 - 需要注意自动拆箱、包装类型缓存(Integer 缓存范围-128 到 127)、字符串常量池的影响。
java
new Integer(123) == 123; //true, 自动拆箱 new Integer(123) == new Integer(123); //false, 对象地址不同 (Integer) 123 == (Integer) 123; //true, 自动装箱,使用缓存 (Integer) 128 == (Integer) 128; //false, 自动装箱,超出缓存范围 "Hello world" == ("Hello" + " " + "world"); //true, 编译期优化,指向常量池同一对象 new String("Hello world") == new String("Hello world"); //false, 对象地址不同
new Integer(123) == 123; //true, 自动拆箱 new Integer(123) == new Integer(123); //false, 对象地址不同 (Integer) 123 == (Integer) 123; //true, 自动装箱,使用缓存 (Integer) 128 == (Integer) 128; //false, 自动装箱,超出缓存范围 "Hello world" == ("Hello" + " " + "world"); //true, 编译期优化,指向常量池同一对象 new String("Hello world") == new String("Hello world"); //false, 对象地址不同
代码可读性
- 魔数 (Magic Number):
- 提取成命名清晰的常量。
- 不要使用数值表示阶段、状态、错误等,应使用枚举。
- 使用
TimeUnit
等工具类提高可读性。- 反例:
Thread.sleep(interval * 60 * 1000);
- 正例:
TimeUnit.MINUTES.sleep(intervalInMinutes);
- 反例:
- 常量 VS 枚举:
- 推荐使用枚举来代替常量类,因为枚举类型更安全,可以携带更多信息(如描述),并且可以包含业务逻辑。
- 不要依赖枚举的
ordinal()
或name()
进行逻辑判断,应为枚举增加value
和name
等属性。
Lambda 表达式
- 优点:简洁、更好的可读性、并行优势。
- 注意:
- 存在初始化开销和拆装箱开销。
- 为保证可读性,调用链不宜过长。
Java"进阶"技术
- 包括:反射、字节码修改、动态代理、注解处理工具、代码生成。
- 尽量避免使用:
- 代码设计: 设计良好的业务代码很少需要使用这些技术。
- 兼容性: 这些技术常作用于非公开 API,违反了开发者的设计意图,难以保证兼容性。
- 性能: 会对应用启动或运行中性能有影响。
其它
- 推荐阅读: 《阿里巴巴 Java 开发手册》。
2. 三方类库
类库选择
- 性能: 参考官方、非官方的 Benchmark、针对应用场景测试。
- 兼容性: 考虑向前、向后兼容。
- 社区: 考察项目使用量和使用者。
- 生态: 考虑功能扩展、与其他类库的集成。
- 服务器端应用程序: 需关注安全性。
- SDK: 应追求最小化依赖。
Lombok
- 优点: 简洁、方便修改。
- 常用注解:
- 对数据类:
@Data
,@NoArgsConstructor
,@AllArgsConstructor
- 对非数据类:
@Getter
,@Setter
,@Slf4j
- 对数据类:
- 不推荐使用:
@Builder
(对数据类): 因为它不符合 JavaBean 规范,且有额外的内存开销。构建者模式的意图是分离复杂对象的构建与表示,例如HttpClientBuilder
。@SneakyThrows
Slf4j
- 是一个日志门面,可以桥接具体的日志实现类库如 log4j, log4j2, Logback。
- Lombok 的
@Slf4j
注解可以自动生成private static final Logger log = ...
。 - 日志打印方式:
- 推荐使用占位符的方式:
log.debug("info: {}", info);
,这种方式可读性好,且在日志级别关闭时没有字符串拼接的性能损耗。 - 不推荐:
log.debug("info: " + info);
或log.debug(String.format(...));
,因为即使日志不打印,也会有无用的字符串拼接和方法调用,影响性能。
- 推荐使用占位符的方式:
- 打印异常堆栈: 不要捕获
Throwable
;不要使用e.printStackTrace()
。- 推荐方式:
log.error("Fail to create domain, cause: ", e);
或使用 Guava 的Throwables.getStackTraceAsString(e)
。
- 推荐方式:
Guava
- 提供了大量实用工具:异常处理、前置条件、字符串处理、集合扩展、缓存、并发库、I/O 等。
其他类库
- caffeine cache: 高性能的 Java 缓存库,取代 Guava Cache,性能更好、内存占用小、命中率高。
- HttpClient: 注意使用连接池,并了解其默认重试 3 次的机制。使用完响应后要调用
EntityUtils.toString()
或EntityUtils.consume()
。 - Joda Time: 已经进入维护阶段,推荐使用 Java 8 自带的 Date API。
- Json 类库: 推荐 Jackson、Gson;不推荐 fastjson、Json-lib。Spring Boot 项目推荐只使用 Jackson。
- XStream: 在不使用自动检测注解的功能时是线程安全的,应避免重复创建 XStream 对象。
3. 工程结构
文档与数据库管理
- 文档管理: 在项目根目录下创建
doc
目录,存放需求、设计、接口文档。 - **数据库变更控制: 在项目根目录下创建
db
目录,用于存放 SQL 脚本,并进行人工版本控制。每次变更新建一个带序号的 SQL 文件。
.gitignore
- 应配置好
.gitignore
文件,忽略 IDE 生成的文件(如.idea
,.iml
)和编译产物(如target/
)。
Maven
- 插件: 配置
maven-compiler-plugin
,maven-resources-plugin
,maven-javadoc-plugin
,maven-source-plugin
等。 - 属性: 使用
<properties>
统一管理依赖版本,消除重复,便于管理。 - 模块: 可以将常量、枚举、RPC 接口、工具类、Dao 等通用代码抽取为独立的 Maven 模块。
- Parent: 不推荐使用
spring-boot-starter-parent
。
应用分层结构
- 分层: 推荐采用《阿里巴巴 Java 开发手册》中的分层结构。
- 开放接口层: 封装 Service 方法暴露成 RPC 接口或 Web 接口。
- Web 层: 访问控制、参数校验、简单处理。
- Service 层: 相对具体的业务逻辑服务层。
- Manager 层: 通用业务处理层,用于封装第三方平台、下沉通用能力(如缓存)、组合多个 DAO。
- DAO 层: 数据访问层,与底层数据库进行数据交互。
- 依赖关系处理: 推荐使用构造器注入依赖,避免在类中直接
new
被依赖类。 - DTO 与 Model:
- DTO(数据传输对象)面向交互,Model 面向业务。
- 不推荐使用
BeanUtils
进行两者之间的复制,因其性能和可修改性问题。推荐的方式是在各自类中提供转换方法或定义一个专门的 Converter。
- Controller:
- 参数校验: 不推荐使用 Bean Validation、Spring Validation。
- 序列化: 不要依赖 DTO 字段的命名,应使用注解(如
@SerializedName
)指定。对于复杂/嵌套层次多的响应,应使用自定义 JsonSerializer。
- Service:
- 线程: 必须通过线程池提供,且不允许使用
Executors
去创建,应通过ThreadPoolExecutor
的方式,以规避资源耗尽的风险。
- 线程: 必须通过线程池提供,且不允许使用
- Dao:
- 实现: 推荐 Mybatis、JDBC。
- SQL: 避免在 Java 代码中拼接 SQL 字符串,以防 SQL 注入。
4. 测试相关
基础原则
- 测试代码侵入: 测试代码不应该侵入业务代码。
- Mock 工具: 避免使用 PowerMock、JMock。
单元测试
- 定义: 测试最小功能单元,比如单个方法。单元测试的难度和代码设计的好坏息息相关。
- DAO 层测试: 可以更换数据源,使用测试数据库或内存数据库(如 H2)。
- Manager 层测试: Mock 掉 Dao 层和三方依赖。
- Service 层测试: Mock 掉 Manager 层和其他 Service。
- Controller 层测试: Mock 掉 Service 层。
- Mock 三方 Rest API: 可以启动内嵌容器,使用 Mock 平台或 Spring Boot 的
MockRestServiceServer
。
集成测试与其它
- 集成测试: 组合代码单元,测试组合结果。
- 工具:
- 接口测试:Postman
- 压测:Jmeter
- 功能测试:Fiddler
- 测试覆盖率: 衡量测试的完整性。
评论
暂无评论,来发表第一条评论吧