所有分类
  • 所有分类
  • 未分类

Java后端开发常用规范

简介

本文介绍Java后端开发的一些规范。持续更新。

本规范是本人在实践中总结出来的,可提高项目的可维护性、提高扩展性、提高开发速度。本文可以解决项目中效率低下、难以维护、让人心累的痛点等问题。

本文所有的规范,全都不是一拍脑袋就写出来的,全都是经过了如下步骤:

  1. 经历了错误做法带来的问题
  2. 在项目中使用过本文的正确做法(解决了错误做法的问题)
  3. 查询过网上各种观点的信息
  4. 经过自己的多重考虑

本文的规范,有的你也许不知道是好还是不好,我可以这么说:刚开始我同事也是不知道好还是不好,用了之后跟我说:真香!

比如:我刚入门Java时觉得controller、service、mapper、entity这几个大包比较好,后来是我一个技术很强的组长率先使用了新方式:每个表一个包,里边进行拆分。用了之后,一周以内我就发现了它的好处。

题外话:如果一个人技术很强(比如写过开源项目、是CSDN博客专家等),如果你和他有不一样的见解,可以先假设他是对的,然后再进行尝试与验证,这样你才能进步。不要直接去否定技术大牛,除非你达到或者超越了人家。

项目的版本指定

版本号必须是-SNAPSHOT结尾。(版本号也就是pom.xml的version标签)。

因为业务代码更新会很频繁,使用-SNAPSHOT结尾可以保证每次从maven私库去拉新代码。

不用-SNAPSHOT结尾的缺点

如果不使用-SNAPSHOT结尾,会导致先从maven本地去取,若更新了代码,很难去更新本地依赖。虽然可以将maven设置为每次都从私库更新,但是这会导致所有依赖都从私库去拉,构建会很慢!

也许你想到了一个保证拉取最新代码的方法:每次改动代码都去修改版本号。虽然可行,但业务代码的改动会很频繁,这会导致版本超级多!这种方法只适用于开源项目或者变动不频繁的项目。

项目的模块划分

模块的划分

单个项目分为:xxx-api模块、xxx-core模块,加一个pom.xml。比如订单项目,分为:order-api模块、order-core模块、pom.xml。

  • xxx-api:用于让其他项目使用(引入依赖)。包括:bo、vo、本项目的feign定义。
    • xxx-api放的应该是本项目供其他项目调用的feign。想调本项目的其他项目直接引入这个xxx-api即可。如果自己想用feign调用其他项目,让其他项目把feign放到他们自己的yyy-api中。
      • 这种方式与dubbo的写法类似,符合rpc方法暴露和调用流程。
  • xxx-core:主体项目。包括:业务(Controller、Service、Mapper、Entity)、配置类等。
  • pom.xml:作为这个业务项目的父依赖,里边包含公共依赖等。

示意图:

顶层pom.xml示例:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.knife.example</groupId>
        <artifactId>common-parent</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/>
    </parent>

    <artifactId>order</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <packaging>pom</packaging>
    <name>order</name>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <modules>
        <module>order-api</module>
        <module>order-core</module>
    </modules>

</project>

上边parent标签的内容是公共项目里的,这个公共项目包含:核心组件、引入公共依赖,将spring-boot-starter-parent作为parent标签内容。common-parent的作用是:作为common项目的顶级项目,并给各个业务做parent。

order-api的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.knife.example</groupId>
        <artifactId>common-dep-api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/>
    </parent>

    <artifactId>order-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>order-api</name>
    <packaging>jar</packaging>
    <description>Demo project for Spring Boot</description>

    <dependencies>
    </dependencies>

</project>

上边parent标签的内容是公共项目里的,common-dep-api的作用是:作为各个业务的api的parent,里边有公共依赖等。

order-core的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.knife.example</groupId>
        <artifactId>common-dep-core</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>order-core</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>order-core</name>
    <packaging>jar</packaging>
    <description>Demo project for Spring Boot</description>

    <dependencies>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>

        <resources>
            <!--先只取bootstrap.yml等,不取application*.yml文件-->
            <resource>
                <directory>src/main/resources</directory>
                <excludes>
                    <exclude>application*.yml</exclude>
                </excludes>
                <!-- 是否替换yml或者properties里@xx@表示的maven properties属性值 -->
                <filtering>true</filtering>
            </resource>

            <!--添加application.yml等文件-->
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>application.yml</include>
                    <include>application-${profileActive}.yml</include>
                    <!--<include>**/application-${profileActive}.yml</include>-->
                </includes>
                <!-- 是否替换yml或者properties里@xx@表示的maven properties属性值 -->
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

</project>

上边parent标签的内容是公共项目里的,common-dep-core的作用是:作为各个业务的core的parent,里边有公共依赖等。 ​

业务代码的结构

  1. 每个表对应一个包,里边是包含的包有:controller、service、mapper、entity等
  2. 添加一个facade包(Controller调用Facade,Facade调用Service或Mapper)
    1. 使用MyBatis-Plus后,应该将Service和Mapper看为同一级别,都作为数据访问层,新建facade包作为业务层,这样业务多了可以在facade层拆分。

例如:

 优点

  1. 模块化,业务分离清晰
  2. 开发速度快(只需关注自己模块代码即可)

思考

有其他的划分方式是:xxx-api、xxx-common、xxx-entity、xxx-service、xxx-web。个人感觉这样的划分很差。它的缺点是:

  1. 不符合业务模块化思想,后期很难拆分或合并项目
  2. 开发速度慢(同一个业务的Entity、Service、Controller被拆开了,写的时候看代码要吐血)

表名对应包下的包的命名规范

  1. controller:Controller集合
  2. facade:调用Service的逻辑集合,要包含接口和实现类,实现类用@Component注入
  3. service:MyBatis-Plus的Service,里边不要写任何代码,只用于被facade调用。
  4. mapper:MyBatis-Plus的Mapper,被service调用。如果要写SQL,facade直接去调即可。
  5. entity:MyBatis-Plus的DAO(数据库对象)。
  6. bo:入参实体类
  7. vo:返回值实体类
  8. constant:常量。比如:枚举类、Interface常量类
  9. helper:业务工具,比如:组装实体类的字段
  10. schedule:定时任务,比如:xxl-job的定时任务
  11. mq:mq消费者
  12. strategy:策略模式(假如用到策略模式的话)。其他设计模式也一样,单独写一个包,以设计模式的名字命名。

备注

本处说业务项目要分为api和core,不是所有项目。一个项目一般要这么分:

  1. 一个common项目
    1. 可分为:
      1. common-parent:父项目(指定SpringBoot为父项目)
      2. common-dep-api:业务api层的依赖
      3. common-dep-core:业务core层的依赖
      4. common-core:业务的公共配置、工具类、全局处理等。
  2. 多个业务应用项目
    1. 分为api和core

单个模块的包的划分

包结构的整体改造

因为Mybatis-Plus的service有大量自带的逻辑,为了与业务逻辑区分,将它看成是数据交互层(与Mapper同一层次)。

对原来的controller=> service=> mapper结构进行改造,改为:controller=> facade=> service=> mapper

所以,不要在service里写任何业务逻辑,只能写与数据交互的逻辑。所有的业务逻辑写到facade中,facade内部分成抽象层和实现层。如果业务太多,就在facade里拆成多个。

包名规定

划分为如下几个包:

  1. controller
    1. 接口,不要写任何业务代码,必须直接用一行代码去调用facade。
  2. facade
    1. 所有业务代码写到这里,读写数据库时去调用service或者mapper
    2. 要写接口和实现类,实现类用@Component注入
  3. service
    1. 只存放Mybatis-Plus的service,不写任何业务代码。
  4. mapper:
    1. MyBatis-Plus的Mapper。如果要写SQL,facade直接调,不要用service调mapper。
  5. entity
    1. 存放数据库表对应的实体类
  6. bo
    1. 方法的入参。所有的实体类名都以BO结尾,比如:UserBO。
  7. vo
    1. 方法的返回值。所有的实体类名都以VO结尾,比如:UserVO。
  8. dto
    1. 内部使用的实体类。(尽量不要使用此命名,因为分不清是入参还是返回值,不清晰)
    2. 所有的实体类名都以DTO结尾。
  9. constant
    1. 常量。比如:枚举类、Interface常量类。
  10. helper
    1. 业务工具,比如:组装实体类的字段。工具类以Helper结尾,比如:UserHelper。
    2. 不用写接口和实现类,直接写实现类即可,若需要注册为Bean,用@Component。
  11. schedule
    1. 定时任务,比如:xxl-job的定时任务。类名都以Schedule结尾,比如:UserSchedule。
  12. mqConsumer
    1. mq消费者。类名都以MqConsumer结尾,比如:UserMqConsumer。
  13. strategy
    1. 策略模式(假如用到策略模式的话)。
    2. 其他设计模式也一样,单独写一个包,以设计模式的名字命名。

使用枚举(不要用数字)

说明

要用枚举来表示类型,不要用数字。比如:有三种支付方式:微信、支付宝、银行卡,则这样定义枚举:

package com.example.pay;
 
public enum PayType {
    ALIPAY("支付宝支付"),
    WECHAT_PAY("微信支付"),
    BANK_CARD_PAY("银行卡支付")
    ;
 
    /**
     * 描述
     */
    private final String description;
 
    PayType(String description) {
        this.description = description;
    }
 
    public String getDescription() {
        return description;
    }
}

所有用到的地方都用枚举来表示。比如:

  1. Controller:会自动将前端传过来的字符串转为枚举类(根据name()来转换)。
  2. Entity:写数据:自动将枚举对象的name()值写入数据库;读数据:根据name()转为枚举

详细用法见:SpringBoot-在Entity(DAO)中使用枚举类型 – 自学精灵

优点

可读性好

不要用数字表示类型

1:支付宝支付;2:微信支付;3:银行卡支付

原因:可读性极差,排查问题也麻烦。比如:前端页面上看到了2这个类型,还要看接口文档或者问后端这是什么意思,浪费时间!

接口文档

说明

使用Knife4j+Apixfox或者Knife4j+Yapi

ApiFox用法

用法1:项目起来后,通过knife4j的“分组Url”去导入到Apifox。
用法2:使用Idea的Apifox Helper插件将Knife4j的数据同步到Apifox。

Yapi用法

Knife4j-将接口信息同步到Yapi – 自学精灵

Knife4j

Knife4j的用法见这里。例如:

优点

  1. 减少接口文档的代码冗余
  2. 可快速导入接口

git提交规范

下边的git提交流程与规范是宝贵经验,它能解决如下问题:

  1. 分支差距过大,导致合代码无数的冲突
  2. 合完代码后发现代码丢失
  3. 分支不清晰,无法追溯问题
  4. 合代码耗时很长,占用大量时间。

先来说明git的基本使用规范

  1. git用户名要指定为名字拼音的第一个(或前两个)字母。比如:李四(ls),张三(zhs)
    1. 不要搞稀奇古怪的英文,因为这样追溯代码时不好找对应的人。
  2. 要选择rebase,禁用merge
    1. merge会丢代码(我周围的人踩过很多这个坑)。
    2. git提交清晰
    3. rebase是人类的正常思维:远程的代码优先。
      1. rebase是本地git先跟上远程git的最新提交点,再去提交代码
      2. merge是让远程git以本地git为基点(这样会导致本来领先的远程git又退回了)

IDEA选择rebase的方法

拉代码

推代码

项目从0-1时

说明

将git分支分为主分支和临时分支。

  • 开发阶段:
    • develop(只有这一个分支)
  • 测试阶段:
    • 开发完毕后从develop新拉分支,命名为test,用于测试(develop分支废弃)
    • 若有新需求:
      • 从test新拉临时分支写代码,分支命名为:test_需求名
      • 代码写完后,压点,cherry pick到test。(合到test的只有一个提交点,若test已更新,要选择rebase,不要选择merge)
    • 若有bug:
      • 小bug:直接在test改
      • 大bug:方法与上边“若有新需求”一致。
  • 上线阶段:
    • 测试完毕后从test新拉分支,命名为prod,用于测试

上线完毕后,项目0-1阶段结束,开启1-100阶段。删除develop分支,新代码全部从prod新拉分支写。

项目从1-100时

说明

将git分支分为主分支和临时分支。

  • 主分支:test(测试)、pre(预发布)、prod(生产)
  • 临时分支:需求点和bug修改

开发与提交流程

  1. 每个修改点(需求或bug)都要从prod新拉分支
  2. 合代码(合代码时都是从临时分支cherry pick到目的分支(主分支))
    1. 往test分支合代码时,需要先把自己的临时分支压缩为一个点,再cherry pick到test。
    2. 往pre分支合代码时,从临时分支cherry pick到pre分支,不要从test分支cherry pick。(因为test肯定有没测试的,不能上pre)
    3. 往prod分支合代码时,组员告诉组长自己的提交点,由组长从临时分支cherry pick到prod分支(因为pre肯定有没测试的,不能上正式)
  3. 远程有更新时,要rebase(以远程为基准),不要用merge(以本地为基准)
  4. 修改点上线(临时分支cherry pick到master)后,删除临时分支(防止分支过多)
  5. 定期(两三周)对test进行清理,删除test并重新从prod拉分支,作为test分支。(防止test与prod差距较远,导致临时分支往test分支合代码时冲突很多)
  6. 定期(两三周)对pre进行清理,删除pre并重新从prod拉分支,作为pre分支。(防止pre与prod差距较远,导致临时分支往pre分支合代码时冲突很多)

优点

以上步骤是我之前所在某个公司的提交流程,按这个流程来做,可以做到:合代码基本不出问题、合代码速度快(一般不会超过3分钟)。

以上步骤每一步都是有原因的:

  • 从prod拉新分支:可保证新分支代码是基于生产的,可以保证新分支是纯粹的自己的修改点
  • 合代码时都是从临时分支cherry pick到目的分支:可保证不会将其他人代码合到目的分支
  • 定期删除test、pre并从prod拉分支:从临时分支合到主分支时基本不会有冲突;而且可以删除test里无用的代码

感言

一个正常的功能点,如果合代码超过10分钟,那么,项目的git管理大概率有问题。如果超过30分钟,项目的git管理问题有点儿大。如果超过一个小时,那么这个项目肯定是经常丢代码,经常出奇怪的线上问题,客户投诉率肯定很高(亲身经历)。

MQ要自动注册

说明

无论用的是哪种MQ(RabbitMQ、RocketMQ、Kafka),都会需要将Topic、队列等信息写入到MQ服务端(Broker)。

写入服务端有两种方式:

  1. 手动在MQ服务端的Web管理页面上添加
  2. 写在代码里
    1. 这样项目在启动时,会自动往MQ服务端上注册。

要使用第2种方法(自动注册),不要使用第1种方法(手动添加)。

优点

  1. 发布功能时省心
    1. 假设代码有test(测试)、pre(预发)、prod(生产)三个环境
      1. 若是自动注册的:只需将代码合到相应分支即可,项目启动时自动往MQ服务端注册
      2. 若是手动添加的:需要手动在三个环境添加信息
  2. MQ服务端更改部署环境或者重装时无需手动处理
    1. 如果MQ服务端更改了,自动和手动的情况如下:
      1. 若是自动注册的:重启项目即可。
      2. 若是手动添加的:需要手动在新环境添加信息

MQ消费端要持久化消息

说明

MQ消费端在收到消息之后,要先保存到数据库或ES,再进行消费的操作。消费失败的要

把失败的原因和消息都保存一下(保存到数据库或ES)。

MQ消费端程序要有个可以直接调用的入口,比如:Controller或者XXL-JOB定时任务,这样等消费端问题修复后,可以手动重新调用消费端程序来消费。

优点

  1. 提高程序的可用性
  2. 排查问题方便
  3. 紧急情况下,可以手动补偿

备注

如果没有进行消息持久化,会有如下缺点:

  1. 消费者消费失败时默认会一直重试,影响其他消息的消费。
  2. 出问题之后不太好立刻解决(难以手动补偿)
2

评论1

请先

  1. 确实,读完提升了我的知识广度!
    543862544 2023-09-26 0
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录