简介
本文介绍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)中使用枚举类型 – 自学精灵
优点
可读性好
不要用数字表示类型
1:支付宝支付;2:微信支付;3:银行卡支付
原因:可读性极差,排查问题也麻烦。比如:前端页面上看到了2这个类型,还要看接口文档或者问后端这是什么意思,浪费时间!
接口文档
说明
使用Knife4j+Apixfox或者Knife4j+Yapi。
ApiFox用法
用法1:项目起来后,通过knife4j的“分组Url”去导入到Apifox。
用法2:使用Idea的Apifox Helper插件将Knife4j的数据同步到Apifox。
Yapi用法
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定时任务,这样等消费端问题修复后,可以手动重新调用消费端程序来消费。
优点
- 提高程序的可用性
- 排查问题方便
- 紧急情况下,可以手动补偿
备注
如果没有进行消息持久化,会有如下缺点:
- 消费者消费失败时默认会一直重试,影响其他消息的消费。
- 出问题之后不太好立刻解决(难以手动补偿)
请先
!