Java 开发经验分享

2.5k 字
本文从开发规范、三方类库选择、工程结构设计和测试相关等几个方面介绍了 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
    • 如果出现多个(不同的)字段修饰符,习惯上(尽管不是必需的)它们应按照上述顺序出现。

静态代码检查工具

  • 工具: 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;
      }
  • 注释掉的代码: 应该删除,如果要找回来请通过版本控制。

  • 测试用的 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() 进行逻辑判断,应为枚举增加valuename等属性。

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
  • 测试覆盖率: 衡量测试的完整性。

评论

后继续评论需要管理员审核后可见

暂无评论,来发表第一条评论吧