Java 开发规范

通用规范

通用编码规范请参考 阿里巴巴 Java 开发手册-1.4.0。 里面的编码规范需要各位开发同学都熟悉一下。

父 POM

内部所有的 Java 项目采用统一的父 POM:

<parent>
	<groupId>cn.meiyike.boot</groupId>
	<artifactId>myk-lib-parent</artifactId>
	<version>1.0.0-SNAPSHOT</version>
</parent>

该 POM 统一了外部依赖版本和内部类库版本号,并对项目构建和部署设置了合适的配置。

包布局

Java 后台项目采用 Spring Cloud 来微服务化,相关包布局约定遵照以下布局结构。

以虚拟的作业业务为例, groupId 为 cn.meiyike.cloud , artifactId 为 myk-cloud-homework, 应用中相关包结构参考:

cn.meiyike.cloud
|- homework/
  |- HomeworkApplication.java    # 应用主入口,需放置在最外层,并注释 @SpringBootApplication 或 @SpringCloudApplication
  |- config/                       # config 为 Spring Bean 配置目录
  |- core/                       # 核心业务层
    |- model/                    # 业务 Model
    |- service/                  # 业务服务接口
      |- IHomeworkService.java     # 业务接口都以 I 前缀开头,表示 Interface
      |- impl/                   # 业务服务实现
        |- HomeworkService.java    # 业务实现继承对应接口,并且名称中去除 I 前缀即可
  |- db/                         # 数据库层
    |- config/
    |- model/                    # 数据库 Model
    |- mapper/                   # Mybatis Mappers
      |- HomeworkMapper.java     # 遵循 Mybatis 约定,数据库访问接口以  Mapper 后缀
      |- HomeworkMapper.xml      # 为方便查找 Mapper 文件,Mapper xml 可以放置在 src/main/java 目录
  |- mongo/                      # MongoDB 层
    |- model/
    |- repository
  |- mq/                         # 消息队列层
    |- listener/                 # 消息监听
    |- model/
  |- schedule/                   # 定时任务层
    |- task/                     # 应用内定时任务
    |- job/                      # Quartz 定时任务
  |- web/                        # Web 层
    |- model/
    |- controller/               # 控制器层

API 文档

后台微服务化后,一个非常重要的产出就是 API 接口文档。 后台项目应该尽早开始进行 API 接口文档的设计,并和前端组、APP 组进行提前沟通反馈,优先完善 API 设计。

项目 API 文档采用 OpenAPI(原 Swagger) 规范进行编写。 当前项目采用的 OpenAPI 规范版本依旧是 2.0 版本,后续等所有相关工具都升级到 3.0 版本时,再考虑采用 OpenAPI 3.0 规范。

后台项目在技术设计过程中可以考虑在项目中优先编写 web 层的 model 和 controller ,先通过工具产生文档来进行沟通讨论。

每个 Java 文件都需要添加靠谱歌的 Copyright 。

/**
 * Copyright (c) 2019-${year} Meiyike Inc. to Present.
 * All Rights Reserved.
 *
 * This software is the confidential and proprietary information of Meiyike Inc. .
 * You shall not disclose such confidential information
 * and shall use it only in accordance with the terms of the license
 * agreement you entered into with www.meiyike.cn.
 *
 */

具体 IDE 中的设置可参考 开发工具篇相关部分

用户数据身份标识

用户可能有多个身份,如一个教师可能是某公立学校的老师,也可能在业余时间兼职某培训机构的教师,这两部分的数据是需要区分开的。 为了数据的隔离性,在设计数据存储时,涉及存储用户信息的,需要同时存储 userIdschoolId 作为对应数据的用户身份标识。

数据库开发

请参考 数据库开发规范

Java 数据库开发相关内容:

  • 数据库采用 MySQL , 默认字符串为 utf8
  • 数据库连接池可采用 Spring Boot 默认的数据库连接池或 druid 连接池
  • 简单的数据库访问可以直接使用 spring-jdbc 的 JdbcTemplate
  • 一般的数据库访问通过 MyBatis 进行
  • MyBatis 接口以 Mapper 作为后缀
  • MyBatis SQL 定义的 XML 文件可以和对应的 Mapper 一起放在 src/main/java 相应目录下,便于查找。(父 POM 使用 myk-lib-parent 的前提下)
  • SQL 中的 SQL 关键字,如 SELECTFROMWHEREJOINIS NULL 等统一使用大写;对表名统一使用小写;字段名根据需要写小驼峰写法。
  • SELECT 语句尽可能列出相应的字段名
  • 分页

具体数据库开发的搭建请参考 myk-boot-mybatis

MongoDB 开发

由于 spring-data-mongo 需要迁就 spring-data 下的各种相关技术,因此添加了额外的复杂度。 MongoDB Java 驱动本身在 3.5 版本中添加了 POJO 的支持,因此我们选择直接使用原生的 Java 驱动进行开发。

引入依赖:

<dependency>
  <groupId>cn.meiyike.boot</groupId>
  <artifactId>myk-boot-mongo</artifactId>
</dependency>
cn.meiyike.cloud.homework/
|- mongo/
  |- config/
    |- MongoConfig.java
  |- model/                    # Model
    |- Answer.java
  |- repository/               #
    |- AnswerRepository.java   #

MongoDB 的文档和 Model 中的一个对象一一对应。 大部分基础类型可以直接作对应, myk-data-bson 中也添加了少量注释, 连同官方的注释一起帮助 Java Pojo 和 MongoDB 文档做对应。

public class Answer {
  @_id
  @Oid
  private String id;
  private Long answerId;
  private String content;
}

MongoDB 访问代码直接使用 MongoCollection 。

@Repository
public class AnswerRepository {
	@Resource
	private MongoDatabase db;
	private MongoCollection<Answer> answers;

	@PostConstruct
	public void afterPropertiesSet() {
		answers = db.getCollection("hw.answer", Answer.class);
	}

  public void add(Answer answer) {
		answers.insertOne(answer);
	}
  ...
}

具体 MongoDB Java 驱动使用方式请参考 官方文档

Redis 开发

请参考 Redis 开发规范

  • spring-data-redis 针对 Redis 做了简单封装,因此推荐直接使用 spring-data-redis 进行 Redis 的访问。
  • 默认 RedisTemplate 使用 JDK 的序列化机制,可以根据情况定制 Redis 的值的序列化机制
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
cn.meiyike.cloud.homework/
|- redis/
  |- config/
    |- RedisConfig.java
@Configuration
public static RedisConfig {
  @Bean
  public HessianRedisSerializer hessianRedisSerializer() {
    return new HessianRedisSerializer();
  }

  @Bean
  public RedisTemplate myRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
    template.setsetDefaultSerializer(hessianRedisSerializer());
		return template;
  }
}

消息队列

应用间异步消息传递采用 RabbitMQ ; Spring 对 AMQP (RabbitMQ) 提供了官方支持,因此我们建议直接使用 spring-amqp 来访问消息队列。

  • RabbitMQ 消息队列存在 exchange 和 queue , 其命名规范分别为 e.{service}.{business}q.{service}.{business}。 其中 {service} 为监听的服务的名称;{business} 根据情况可以有多个层级。 如 exchange 为 e.homework.correct,queue 为 q.homework.correct
  • 发送方和接收方需协调好消息的序列化机制
  • 消息发送方可以使用 RabbitTemplate 来发送消息
  • 消息接收方可以使用 @RabbitListener 来简单地建立监听器
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
@EnableRabbit
@Configuration
public class RabbitConfig {

	@Bean
	public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
			SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
		SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
		configurer.configure(factory, connectionFactory);
		@SuppressWarnings("unchecked")
		FatalExceptionStrategy strategy = new YunguFatalExceptionStrategy();
		factory.setErrorHandler(new ConditionalRejectingErrorHandler(strategy));
		return factory;
	}

}
@Service
public class DeliveryService implements IDeliveryService {
  @Resource
	private RabbitTemplate rabbitTemplate;

  private void deliver(Delivery delivery) {
		rabbitTemplate.send("e.balance.delivery", null, JsonMessages.toMessage(delivery));
	}
}
@Component
public class DeliveryListener {

	private static final Logger LOG = LoggerFactory.getLogger(DeliveryListener.class);
	@Resource
	private IDeliveryService deliveryService;

	@RabbitListener(bindings = @QueueBinding(
			value = @Queue(value = "q.balance.delivery", durable = "true"),
			exchange = @Exchange(value = "e.balance.delivery", durable = "true")))
	public void handlerDelivery(Message msg) {
		Delivery payload = JsonMessages.parse(msg, Delivery.class);
		LOG.debug("received delivery {}", payload);
		// TODO
	}

}