原文网址:Java后端开发常用规范_IT利刃出鞘的博客-CSDN博客
简介
本文介绍Java后端开发的一些规范。持续更新。
本规范是本人在实践中总结出来的,可提高项目的可维护性、提高扩展性、提高开发速度。本文可以解决项目中效率低下、难以维护、让人心累的痛点等问题。
本文所有的规范,全都不是一拍脑袋就写出来的,全都是经过了如下步骤:
- 经历了错误做法带来的问题
- 在项目中使用过本文的正确做法(解决了错误做法的问题)
- 查询过网上各种观点的信息
- 经过自己的多重考虑
本文的规范,有的你也许不知道是好还是不好,我可以这么说:刚开始我同事也是不知道好还是不好,用了之后跟我说:真香!
比如:我刚入门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-api放的应该是本项目供其他项目调用的feign。想调本项目的其他项目直接引入这个xxx-api即可。如果自己想用feign调用其他项目,让其他项目把feign放到他们自己的yyy-api中。
- 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,里边有公共依赖等。
业务代码的结构
- 每个表对应一个包,里边是包含的包有:controller、service、mapper、entity等
- 添加一个facade包(Controller调用Facade,Facade调用Service或Mapper)
- 使用MyBatis-Plus后,应该将Service和Mapper看为同一级别,都作为数据访问层,新建facade包作为业务层,这样业务多了可以在facade层拆分。
例如:
优点
- 模块化,业务分离清晰
- 开发速度快(只需关注自己模块代码即可)
思考
有其他的划分方式是:xxx-api、xxx-common、xxx-entity、xxx-service、xxx-web。个人感觉这样的划分很差。它的缺点是:
- 不符合业务模块化思想,后期很难拆分或合并项目
- 开发速度慢(同一个业务的Entity、Service、Controller被拆开了,写的时候看代码要吐血)
表名对应包下的包的命名规范
- controller:Controller集合
- facade:调用Service的逻辑集合,要包含接口和实现类,实现类用@Component注入
- service:MyBatis-Plus的Service,里边不要写任何代码,只用于被facade调用。
- mapper:MyBatis-Plus的Mapper,被service调用。如果要写SQL,facade直接去调即可。
- entity:MyBatis-Plus的DAO(数据库对象)。
- bo:入参实体类
- vo:返回值实体类
- constant:常量。比如:枚举类、Interface常量类
- helper:业务工具,比如:组装实体类的字段
- schedule:定时任务,比如:xxl-job的定时任务
- mq:mq消费者
- strategy:策略模式(假如用到策略模式的话)。其他设计模式也一样,单独写一个包,以设计模式的名字命名。
备注
本处说业务项目要分为api和core,不是所有项目。一个项目一般要这么分:
- 一个common项目
- 可分为:
- common-parent:父项目(指定SpringBoot为父项目)
- common-dep-api:业务api层的依赖
- common-dep-core:业务core层的依赖
- common-core:业务的公共配置、工具类、全局处理等。
- 可分为:
- 多个业务应用项目
- 分为api和core
单个模块的包的划分
包结构的整体改造
因为Mybatis-Plus的service有大量自带的逻辑,为了与业务逻辑区分,将它看成是数据交互层(与Mapper同一层次)。
对原来的controller=> service=> mapper结构进行改造,改为:controller=> facade=> service=> mapper
所以,不要在service里写任何业务逻辑,只能写与数据交互的逻辑。所有的业务逻辑写到facade中,facade内部分成抽象层和实现层。如果业务太多,就在facade里拆成多个。
包名规定
划分为如下几个包:
- controller
- 接口,不要写任何业务代码,必须直接用一行代码去调用facade。
- facade
- 所有业务代码写到这里,读写数据库时去调用service或者mapper
- 要写接口和实现类,实现类用@Component注入
- service
- 只存放Mybatis-Plus的service,不写任何业务代码。
- mapper:
- MyBatis-Plus的Mapper。如果要写SQL,facade直接调,不要用service调mapper。
- entity
- 存放数据库表对应的实体类
- bo
- 方法的入参。所有的实体类名都以BO结尾,比如:UserBO。
- vo
- 方法的返回值。所有的实体类名都以VO结尾,比如:UserVO。
- dto
- 内部使用的实体类。(尽量不要使用此命名,因为分不清是入参还是返回值,不清晰)
- 所有的实体类名都以DTO结尾。
- constant
- 常量。比如:枚举类、Interface常量类。
- helper
- 业务工具,比如:组装实体类的字段。工具类以Helper结尾,比如:UserHelper。
- 不用写接口和实现类,直接写实现类即可,若需要注册为Bean,用@Component。
- schedule
- 定时任务,比如:xxl-job的定时任务。类名都以Schedule结尾,比如:UserSchedule。
- mqConsumer
- mq消费者。类名都以MqConsumer结尾,比如:UserMqConsumer。
- strategy
- 策略模式(假如用到策略模式的话)。
- 其他设计模式也一样,单独写一个包,以设计模式的名字命名。
使用枚举(不要用数字)
说明
要用枚举来表示类型,不要用数字。比如:有三种支付方式:微信、支付宝、银行卡,则这样定义枚举:
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;
}
}
所有用到的地方都用枚举来表示。比如:
- Controller:会自动将前端传过来的字符串转为枚举类(根据name()来转换)。
- Entity:写数据:自动将枚举对象的name()值写入数据库;读数据:根据name()转为枚举
详细用法见:SpringBoot--在Entity(DAO)中使用枚举类型_IT利刃出鞘的博客-CSDN博客
优点
可读性好
不要用数字表示类型
1:支付宝支付;2:微信支付;3:银行卡支付
原因:可读性极差,排查问题也麻烦。比如:前端页面上看到了2这个类型,还要看接口文档或者问后端这是什么意思,浪费时间!
接口文档
说明
使用Knife4j+Apixfox(推荐)。(不推荐Knife4j+Yapi,因为同步接口时有些麻烦。)
用法1:项目起来后,通过knife4j的“分组Url”去导入到Apifox。
用法2:使用Idea的Apifox Helper插件将Knife4j的数据同步到Apifox。
Knife4j
Knife4j的用法见这里。例如:
优点
- 减少接口文档的代码冗余
- 可快速导入接口
git使用流程
MQ要自动注册
说明
无论用的是哪种MQ(RabbitMQ、RocketMQ、Kafka),都会需要将Topic、队列等信息写入到MQ服务端(Broker)。
写入服务端有两种方式:
- 手动在MQ服务端的Web管理页面上添加
- 写在代码里
- 这样项目在启动时,会自动往MQ服务端上注册。
要使用第2种方法(自动注册),不要使用第1种方法(手动添加)。
优点
- 发布功能时省心
- 假设代码有test(测试)、pre(预发)、prod(生产)三个环境
- 若是自动注册的:只需将代码合到相应分支即可,项目启动时自动往MQ服务端注册
- 若是手动添加的:需要手动在三个环境添加信息
- 假设代码有test(测试)、pre(预发)、prod(生产)三个环境
- MQ服务端更改部署环境或者重装时无需手动处理
- 如果MQ服务端更改了,自动和手动的情况如下:
- 若是自动注册的:重启项目即可。
- 若是手动添加的:需要手动在新环境添加信息
- 如果MQ服务端更改了,自动和手动的情况如下:
MQ消费端要持久化消息
说明
MQ消费端在收到消息之后,要先保存到数据库或ES,再进行消费的操作。消费失败的要
把失败的原因和消息都保存一下(保存到数据库或ES)。
MQ消费端程序要有个可以直接调用的入口,比如:Controller或者XXL-JOB定时任务,这样等消费端问题修复后,可以手动重新调用消费端程序来消费。
优点
- 提高程序的可用性
- 排查问题方便
- 紧急情况下,可以手动补偿
备注
如果没有进行消息持久化,会有如下缺点:
- 消费者消费失败时默认会一直重试,影响其他消息的消费。
- 出问题之后不太好立刻解决(难以手动补偿)