后端功能开发
概述
开发服务接口是微服务系统最常规的开发工作之一。编写后端数据层、服务层以及接口层代码,就是常说的 CRUD 工作。
[1]为什么不提供代码生成
这一类工作相对来说比较模式化,所以为了简化重复性工作,很多系统都会提供代码生成工具或者服务,来提升开发效率。
在 Dante Cloud 中并没有提供代码生成工具或服务,仅是提供了各层代码基础性的抽象通用代码,大部分代码还是需要自己生成和编写。
之所以这样做是出于以下几点考虑:
- 不同的公司和团队,都有自己的开发规范和标准。代码生成虽然在一定生可以提高效率,但生成的代码都是模版化的,难以满足所有人的需求,代码生之后还是需要自己手动修改。
- 代码生成难以适应复杂多变的业务,业务逻辑稍微复杂一些、数据表稍微多一点,代码生成就难以支持。代码生成后还是需要大量修改,这不仅会完全抵消代码生成带来的效率,可能还会比自己写投入的精力更大(写代码比改代码效率更高)。
- 单体系统开发人员主要考虑的就是与数据库的交互,但是微服务除了考虑数据库,可能还会涉及服务接口的交互、消息队列的交互、不同存储的交互、分布式事务的处理以及服务拆分的合理性等等。那么这种情况下,代码生成的仅有的优势就完全可以忽略。
[2]为什么选择 Spring Data
为什么使用 JPA 而不是 MyBatis
另外一个比较重要的问题就是 ORM 组件的选择问题。在 Dante Cloud 中,默认使用的是 Spring Date JPA,而没有选择大多数开发人员熟悉的 MyBatis 或 MyBatis Plus。
之所以选择 JPA 是从设计和应用的合理性角度考虑的,而不仅仅是从所谓的“码砖”效率角度考虑。
单体系统玩的就是一个数据库,能够快速灵活的堆叠业务功能,便捷的修改以适应用户需求的变化才是要义。所以在这种场景下,MyBatis 这种面向 SQL 的 ORM 框架的优势就非常大。
虽然微服务系统下,开发效率也是需要考虑的因素,但并不是微服务的重点。微服务的重点是以“服务”的形式沉淀“业务”,这就意味着合理的划分业务以及合理的划分服务,并将两者有机的融合才是微服务优先级最高的考虑事项。
使用微服务架构失败的案例中,最常见的一个典型问题就是“服务”的划分问题。服务划分的过细,就会导致产生大量的服务,不仅大量增加服务间交互加大复杂度,还会增加开发和维护的成本;如果服务划分的过粗,服务数量倒是少了,但是每个服务负责的内容过多,都相当于一个服务就是一个单体项目,这样就失去了微服务系统带来的灵活性和可扩展性。
JPA 的开发是面向对象的,开发时更多考虑的是实体关系的设计及合理性,这就与领域驱动设计异曲同工。领域设计的越合理性越高,服务的划分也越容易。相反,如果使用 MyBatis 之类面向 SQL 的组件,不是说不可以,但是大概率会干扰代码的划分甚至服务划分的合理性。
例如:一个功能涉及多个数据表的关联,在单体项目中这不会有任何问题。但是在服务系统中,可能会出现这几个相关联的表中,其中几个表放入 A 服务更合适,另外几个表放入 B 服务更合适的情况。这样就又需要回过头来重新考虑划分的问题,原来单体使用 MyBatis 一个 SQL 可能就搞定的事情,在微服务中就不得重新编写进行代码拆分。如果使用的是 JPA,那么在编写实体模型时,就会自然而然的考虑到划分以及合理性的问题,相当于提前就进行了思考和设计,减少了不必要的反复和消耗。
Spring Data 的优势
微服务相比单体另外一大优势,就是通过集成的便利性,可以快速的集成多种不同的组件或中间件,来提升整个系统性能。并不是说单体无法进行多内容的集成,而是每增加一种集成就会让系统更加臃肿,甚至需要伤筋动骨式的改造。
微服务的易扩展性,就决定了整个系统不再是单纯的以关系型数据库作为核心,可以根据实际需要灵活的增加 NoSQL数据库、时序数据库等多种类型的存储。
例如:最新版本的 Dante Cloud 中,
Spring Authorization Server
组件的核心数据,就不再简单的支持关系型数据库,还支持了 Redis 和 MongoDB。
选择 Spring Data 的优势就体现出来了,定义好领域模型后,使用相同的模式就可以快速的切换到其它类型的存储上来,
[一]使用JPA开发服务接口
[1]编写实体
使用 JPA 开发服务接口,第一步就是定义实体,就是领域驱动中的“领域层”
说明
定义实体是 JPA 最基础的工作,也是 JPA 最复杂也最难掌握的部分,也是许多人不爱使用 JPA 和 MyBatis 的主要原因。相关的知识较多,不是一句话两句话就可以讲解明白的,所以在本文不会过多讲解基础知识,建议用户自己找相关资料或者书籍系统的学习一下。
下面就以系统中角色相关代码举例,角色的实体定义如下:
@Entity
@Table(name = "sys_role", uniqueConstraints = {@UniqueConstraint(columnNames = {"role_code"})},
indexes = {@Index(name = "sys_role_rid_idx", columnList = "role_id"), @Index(name = "sys_role_rcd_idx", columnList = "role_code")})
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = LogicUpmsConstants.REGION_SYS_ROLE)
public class SysRole extends AbstractSysEntity {
@Id
@UuidGenerator
@Column(name = "role_id", length = 64)
private String roleId;
@Column(name = "role_code", length = 128, unique = true)
private String roleCode;
@Column(name = "role_name", length = 128)
private String roleName;
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = LogicUpmsConstants.REGION_SYS_PERMISSION)
@ManyToMany(fetch = FetchType.EAGER)
@Fetch(FetchMode.SUBSELECT)
@JoinTable(name = "sys_role_permission",
joinColumns = {@JoinColumn(name = "role_id")},
inverseJoinColumns = {@JoinColumn(name = "permission_id")},
uniqueConstraints = {@UniqueConstraint(columnNames = {"role_id", "permission_id"})},
indexes = {@Index(name = "sys_role_permission_rid_idx", columnList = "role_id"), @Index(name = "sys_role_permission_pid_idx", columnList = "permission_id")})
private Set<SysPermission> permissions = new HashSet<>();
public String getRoleId() {
return roleId;
}
public void setRoleId(String roleId) {
this.roleId = roleId;
}
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public Set<SysPermission> getPermissions() {
return permissions;
}
public void setPermissions(Set<SysPermission> permissions) {
this.permissions = permissions;
}
}
说明
@Entity
和@Table
是使用 Spring Data JPA 定义实体必要的注解,标识该实体对应实际的数据表@UniqueConstraint
用于标识该字段是唯一性字段,例如:表的主键就属于唯一性字段@Index
用于给指定的字段添加数据库索引@Cacheable
开启查询缓存@Cache
Hibernate 专有的缓存注解,用于指定缓存的类型以及相关信息AbstractSysEntity
是 Dante Cloud 提供的基础实体定义,其中包含了数据库审计以及常用的字段,继承该类就无需重复定义相关属性。
除了
AbstractSysEntity
基础定义以外,还有多种基础实体定义,例如:支持多租户的AbstractTenantEntity
等等,可以根据实际需求选择使用。
[2]编写Repository
Repository 是 Spring Data 中的核心定义,与领域驱动中的 Repository
概念基本一致。
public interface SysRoleRepository extends BaseJpaRepository<SysRole, String> {
/**
* 根据用户名查找SysUser
*
* @param roleCode 角色代码
* @return {@link SysRole}
*/
@QueryHints(@QueryHint(name = AvailableHints.HINT_CACHEABLE, value = "true"))
SysRole findByRoleCode(String roleCode);
/**
* 根据角色ID查询角色
*
* @param roleId 角色ID
* @return {@link SysRole}
*/
@QueryHints(@QueryHint(name = AvailableHints.HINT_CACHEABLE, value = "true"))
SysRole findByRoleId(String roleId);
}
注意
BaseJpaRepository
是 Dante Cloud 中提供的基础 Repository 定义。继承该类,就可以直接使用常规的增、删、改、查操作。BaseJpaRepository
提供的操作不满足实际需求,就可以在该类中定义自己的方法。@QueryHints
是新版本 Spring Data JPA 中,与缓存配合的注解。该注解会与实体上定义的@Cacheable
注解配合,开启查询的“二级缓存”
[3]编写Service
@Service
public class SysRoleService extends AbstractJpaService<SysRole, String> {
private final SysRoleRepository sysRoleRepository;
public SysRoleService(SysRoleRepository sysRoleRepository) {
this.sysRoleRepository = sysRoleRepository;
}
@Override
public BaseJpaRepository<SysRole, String> getRepository() {
return this.sysRoleRepository;
}
public SysRole findByRoleCode(String roleCode) {
return sysRoleRepository.findByRoleCode(roleCode);
}
public SysRole findByRoleId(String roleId) {
return sysRoleRepository.findByRoleId(roleId);
}
}
注意
AbstractJpaService
是 Dante Cloud 中提供的基础 Service 定义。继承该类,就可以直接使用常规的增、删、改、查操作。AbstractJpaService
提供的操作不满足实际需求,就可以在该类中定义自己的方法。- 系统中还提供有
JpaWriteableService
和JpaReadableService
两个基础 Service 定义。可以根据实际需求灵活选择
之所以有这两个 Service 定义,目的是将数据库的“写”操作和“读”操作分开。因为 JPA 除了支持数据表的映射以外,还支持数据库视图的映射,而视图是无法增、删、改的,即使提供了代码操作执行也会抛错。
[4]编写Controller
@RestController
@RequestMapping("/security/role")
@Tags({
@Tag(name = "用户安全管理接口"),
@Tag(name = "系统角色管理接口")
})
public class SysRoleController extends AbstractJpaWriteableController<SysRole, String> {
private final SysRoleService sysRoleService;
public SysRoleController(SysRoleService sysRoleService) {
this.sysRoleService = sysRoleService;
}
@Override
public BaseJpaWriteableService<SysRole, String> getWriteableService() {
return this.sysRoleService;
}
}
注意
AbstractJpaWriteableController
是 Dante Cloud 中提供的基础 Controller 定义。继承该类,就可以直接使用常规的增、删、改、查接口就已经实现,无需再外编写。AbstractJpaWriteableController
提供的操作不满足实际需求,就可以在该类中定义自己的接口。- 系统中还提供有
AbstractJpaReadableController
以及其它更底层的基础 Controller 定义。可以根据实际需求灵活选择
之所以有这两个 Controller 定义,目的与前面的 Service 一致。是将数据库的“写”操作和“读”操作分开。因为 JPA 除了支持数据表的映射以外,还支持数据库视图的映射,而视图是无法增、删、改的,即使提供了代码操作执行也会抛错。
警告
当前版本的 Dante Cloud 不仅支持阻塞式接口,还支持响应式接口。开发的方法都是类 AbstractJpaWriteableController
类。区别是阻塞式接口,需要继承 cn.herodotus.stirrup.web.api.servlet.AbstractJpaWriteableController
,而响应式接口需要继承 cn.herodotus.stirrup.web.api.reactive.AbstractJpaWriteableController
提示
继承 AbstractJpaWriteableController
抽象类之后,会自动实现新增/修改、根据删除以及分页三个接口。
以系统角色为例,继承 AbstractJpaWriteableController
抽象类之后就会自动实现以下接口:
POST
-/security/role
:新增或修改角色DELETE
-/security/role/{id}
:根据ID删除角色GET
-/security/role
:角色分页查询
[5]编写Config
最后一步,就是编写以上代码的配置类,方便在系统运行时,将相关内容注入到 Spring 中。
示例代码如下所示
@Configuration(proxyBeanMethods = false)
@EntityScan(basePackages = {
"cn.herodotus.stirrup.logic.identity.entity"
})
@EnableJpaRepositories(basePackages = {
"cn.herodotus.stirrup.logic.identity.repository",
})
@ComponentScan(basePackages = {
"cn.herodotus.stirrup.logic.identity.service",
"cn.herodotus.stirrup.logic.identity.controller",
})
public class LogicIdentityConfiguration {
private static final Logger log = LoggerFactory.getLogger(LogicIdentityConfiguration.class);
@PostConstruct
public void postConstruct() {
log.debug("[Herodotus] |- Module [Logic Identity] Configure.");
}
......
}
说明
@Configuration
Spring Boot 配置的注解@EntityScan
Spring Data JPA 扫描实体注解@EnableJpaRepositories
Spring Data JPA 扫描 Repository注解@ComponentScan
Spring Boot 扫描@Service
和@RestController
注解
重要
如果开发的是单模块的 Spring Boot 应用,完全不需要向上面例子一样,手动编写 Configuration
类。因为 @SpringBootApplication
注解会自动帮你你完成类的扫描工作。
如果是多模块的 Spring Boot 应用或者微服务,那么建议一定按照上面的方式,为每一个模块定义单独的 Configuration
类。之所以这样做,有以下几个原因:
- 单模块的 Spring Boot 应用,会以
@SpringBootApplication
注解所在的包为基础,根据当前 Class 的路径主动扫描所有的子包。 - 如果是多模块的 Spring Boot 应用,不同的模块是在不同的 jar 包中。即使在其中一个 jar 包中指定了
@SpringBootApplication
,因为包结构以及代码层次的变化,很可能是无法扫描到其它 jar 包中的类的。 - 有些朋友图方便,在一个类中直接指定扫描基础包,例如:直接扫描
cn.herodotus
。这种方式虽然也可以实现跨 jar 包类的注入,但是需要和不需要的类都会去扫描,会大大降低系统启动的效率 - 按照上面的手动编写
Configuration
类的方式,按需扫描可以极大的降低扫描的范围。而且也更加灵活,在需要的代码中@Import
该Configuration
类就可以使用。
[二]使用MyBatis Plus开发服务接口
如果你不习惯使用 JPA,那么 Dante Cloud 还支持使用 MyBatis Plus 来编写代码。JPA 和 MyBatis Plus 编写的代码可以在 Dante Cloud 中同时运行。
警告
这里所说的 JPA 和 MyBatis Plus 编写的代码可以在一起运行,并不说两者可以进行切换,即:使用 JPA 编写的代码,只能自己手动全部修改为 MyBatis Plus 代码。无法做到通过修改配置,直接进行代码的装换,因为两者的机制完全不同,这是无法做到的。
使用 MyBatis Plus 开发服务接口,与前面使用 JPA 的主要逻辑大同小异,主要的区别在于 MyBatis 的配置。使用 MyBatis Plus 编写完代码之后,要在具体服务的配置文件中,指定 Mapper XML 所在的位置。如下所示:
mybatis-plus:
mapper-locations:
- "classpath*:camunda-extends-mappings/**/*.xml"
[三]使用其它存储开发服务接口
除了支持常规的关系数据库开发服务接口以外,Dante Cloud 还支持以 Redis、MongoDB 和 Cassandra 作为存储介质开发服务接口。
开发基于 Redis、MongoDB 和 Cassandra 的服务接口,与 JPA 的开发方式完全相同,区别在于底层基础通用代码不同,开发时依赖不同的基础数据模块即可。
当前,已支持的基础数据模块有:
data-module-cassandra
:基于Spring Data Cassandra
的数据层基础通用代码模块。data-module-mongodb
:基于Spring Data MongoDB
的数据层基础通用代码模块。data-module-redis
:基于Spring Data Redis
的数据层基础通用代码模块。data-module-jpa
:基于Spring Data JPA
的数据层基础通用代码模块。