基于RPC框架的通用代码实现
在业务代码中,会经常有大量的CRUD这种枯燥无味我代码需要我们去编写。当然,也有现成的工具,比如mybatis generator等工具,让我们脱离手动编写这些代码。但这些仅仅只是针对mapper层,虽然Service层有mybatis-plus中的ServiceImpl,但是Controller层,也需要我们自己编写大量的接口。
在此背景下,我对公司的项目进行了抽象,使得这些基本的CRUD代码全部集成了在一些基类中,子类只需要继承就获得了CRUD的基本能力。因为公司采用的架构,负责与数据库交互的工程单独提了出来,用RPC通信面向C端和B端的工程交互,具体的架构图如下
- nj-cms项目,负责提供后台管理的接口
- nj-zc-app项目,负责提供app端端接口
- nj-zc-business-fp项目,负责与数据库交互
我们要达成的目的是:**基本的CRUD方法,我们不需要写任何代码就能对外暴露接口,并且调用接口数据可以落库、查询等。**
在此之前,先准备一张表
CREATE TABLE `base_car` (
`id` bigint NOT NULL COMMENT '主键id',
`name` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称',
`car_number` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '车牌号',
`car_series` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '车系',
`car_model` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '车型',
`source` int NOT NULL DEFAULT '1' COMMENT '车辆来源, 1.倪家',
`number` int NOT NULL DEFAULT '1' COMMENT '数量',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用,1是0否',
`car_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '车辆基础费',
`timeout_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '超时费',
`reparation_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '赔偿费',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_car_number` (`car_number`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='车辆信息表';
说明:所有表中,都有
id
,create_time
,update_time
这三个字端。id
的生成方式采用_雪花算法_
准备工作
实体类的抽象
id
,create_time
,update_time
每张表都会有,所以可以抽成一个抽象父类的实体类BaseEntity
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @author fangxi created by 2021/11/10
*/
@Data
public abstract class BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 创建时间 默认当前时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间 默认当前时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
注:为了开发方便,这里的使用了mybatis-plus增强工具。
下面是base_car
表的实体类,继承BaseEntity
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nijia.zc.business.fp.config.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* @author fangxi created by 2021/12/13
* 车辆信息表
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "base_car")
public class Car extends BaseEntity {
/**
* 名称
*/
@TableField(value = "name")
private String name;
/**
* 车牌号
*/
@TableField(value = "car_number")
private String carNumber;
/**
* 车系
*/
@TableField(value = "car_series")
private String carSeries;
/**
* 车型
*/
@TableField(value = "car_model")
private String carModel;
/**
* 车辆来源, 1.倪家
*/
@TableField(value = "`source`")
private Integer source;
/**
* 是否启用,1是0否
*/
@TableField(value = "enabled")
private Boolean enabled;
/**
* 车辆基础费
*/
@TableField(value = "car_price")
private BigDecimal carPrice;
/**
* 超时费
*/
@TableField(value = "timeout_price")
private BigDecimal timeoutPrice;
/**
* 赔偿费
*/
@TableField(value = "reparation_price")
private BigDecimal reparationPrice;
/**
* 数量
*/
@TableField(value = "number")
private Integer number;
}
DTO的抽象
因为实体类不应该通过RPC接口暴露出来,所以使用中,我们需要一个DTO对象用于在RPC接口传输数据。
一个实体类对应一个DTO对象,因此我们需要准备两个DTO:BaseDTO
和CarDTO
BaseDTO
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author fangxi created by 2021/12/9
*/
@Data
public class BaseDTO implements Serializable {
private Long id;
/**
* 创建时间 默认当前时间
*/
@ApiModelProperty("创建时间")
private LocalDateTime createTime;
/**
* 更新时间 默认当前时间
*/
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
}
CarDTO,继承BaseDTO
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* @author fangxi created by 2021/12/13
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class CarDTO extends BaseDTO implements Serializable {
/**
* 名称
*/
@ApiModelProperty("名称")
private String name;
/**
* 车牌号
*/
@ApiModelProperty("车牌号")
private String carNumber;
/**
* 车系
*/
@ApiModelProperty("车系")
private String carSeries;
/**
* 车型
*/
@ApiModelProperty("车型")
private String carModel;
/**
* 车辆来源, 1.倪家
*/
@ApiModelProperty("车辆来源,1.倪家")
private Integer source;
/**
* 是否启用,1是0否
*/
@ApiModelProperty("是否启用,1是,0否")
private Boolean enabled;
/**
* 车辆基础费
*/
@ApiModelProperty("车辆基础费")
private BigDecimal carPrice;
/**
* 超时费
*/
@ApiModelProperty("超时费")
private BigDecimal timeoutPrice;
/**
* 赔偿费
*/
@ApiModelProperty("赔偿费")
private BigDecimal reparationPrice;
/**
* 数量
*/
@ApiModelProperty("数量")
private Integer number;
}
mapper的生成
实体类准备完成之后,就可以生成mapper层了,这里使用mybatis generator工具生成即可
CarMapper.java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nijia.zc.business.fp.entity.Car;
import org.apache.ibatis.annotations.Mapper;
/**
* @author fangxi created by 2021/12/13
*/
@Mapper
public interface CarMapper extends BaseMapper<Car> {
}
CarMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nijia.zc.business.fp.mapper.CarMapper">
<resultMap id="BaseResultMap" type="com.nijia.zc.business.fp.entity.Car">
<!--@mbg.generated-->
<!--@Table base_car-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="vin" jdbcType="VARCHAR" property="vin" />
<result column="car_number" jdbcType="VARCHAR" property="carNumber" />
<result column="car_series" jdbcType="VARCHAR" property="carSeries" />
<result column="car_model" jdbcType="VARCHAR" property="carModel" />
<result column="source" jdbcType="INTEGER" property="source" />
<result column="enabled" jdbcType="BOOLEAN" property="enabled" />
<result column="car_price" jdbcType="DECIMAL" property="carPrice" />
<result column="deposit_price" jdbcType="DECIMAL" property="depositPrice" />
<result column="timeout_price" jdbcType="DECIMAL" property="timeoutPrice" />
<result column="reparation_price" jdbcType="DECIMAL" property="reparationPrice" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, vin, car_number, car_series, car_model, `source`, enabled, car_price, deposit_price,
timeout_price, reparation_price, create_time, update_time
</sql>
</mapper>
至此,准备工作已经全部完成,接下来需要编写CRUD的抽象代码
RPC服务提供者抽象实现
RPC是通过接口暴露服务的,所以我们需要一个父类接口,定义这些CRUD方法,我们叫做BaseRemote
import com.nijia.commons.dto.PageResponse;
import com.nijia.zc.business.fp.dto.BaseDTO;
import java.util.List;
import java.util.Map;
/**
* @param <DTO> the type parameter
* @author fangxi created by 2021/12/9
*/
public interface BaseRemote<DTO extends BaseDTO> {
/**
* 根据id查询
*
* @param id the id
* @return dto
*/
DTO get(Long id);
/**
* 查询全部
*
* @return the list
*/
List<DTO> selectAll();
/**
* 保存
*
* @param dto the dto
* @return the boolean
*/
boolean save(DTO dto);
/**
* 批量保存
*
* @param dtoList the dto list
* @return the boolean
*/
boolean save(List<DTO> dtoList);
/**
* 根据id更新
*
* @param dto the dto
* @return the boolean
*/
boolean update(DTO dto);
/**
* 批量根据id更新
*
* @param dtoList the dto list
* @return the boolean
*/
boolean update(List<DTO> dtoList);
/**
* 根据id删除
*
* @param id the id
* @return the boolean
*/
boolean remove(Long id);
/**
* 批量根据id删除
*
* @param ids the ids
* @return the boolean
*/
boolean remove(List<Long> ids);
/**
* 分页查询
*
* @param dto 查询条件
* @param current 当前页
* @param limit 每页数量
* @return the page response
*/
PageResponse<DTO> page(DTO dto, int current, int limit);
/**
* 根据id分组
*
* @param ids the ids
* @return the map
*/
Map<Long, DTO> grouping(List<Long> ids);
}
重点在实现这些方法,这里结合了mybatis-plus的ServiceImpl类,实现类叫做BaseServiceImpl
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nijia.commons.dto.PageResponse;
import com.nijia.commons.dto.PageResponseBuilder;
import com.nijia.commons.utils.CollectionUtils;
import com.nijia.zc.business.fp.config.BaseEntity;
import com.nijia.zc.business.fp.dto.BaseDTO;
import com.nijia.zc.business.fp.remote.BaseRemote;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author fangxi created by 2021/12/9
*/
public abstract class BaseServiceImpl<Mapper extends BaseMapper<Entity>, Entity extends BaseEntity, DTO extends BaseDTO> extends ServiceImpl<Mapper, Entity> implements BaseRemote<DTO> {
@Override
public DTO get(Long id) {
Entity entity = getById(id);
return toDTO(entity);
}
@Override
public List<DTO> selectAll() {
List<Entity> entityList = list();
return CollectionUtils.isEmpty(entityList) ? Collections.emptyList() : entityList.stream().map(this::toDTO).collect(Collectors.toList());
}
@Override
public boolean save(DTO dto) {
return save(toEntity(dto));
}
@Override
public boolean save(List<DTO> dtoList) {
List<Entity> entities = dtoList.stream().map(this::toEntity).collect(Collectors.toList());
return saveBatch(entities);
}
@Override
public boolean update(DTO dto) {
Entity entity = toEntity(dto);
return updateById(entity);
}
@Override
public boolean update(List<DTO> dtoList) {
List<Entity> entities = dtoList.stream().map(this::toEntity).collect(Collectors.toList());
return updateBatchById(entities);
}
@Override
public boolean remove(Long id) {
return removeById(id);
}
@Override
public boolean remove(List<Long> ids) {
return removeByIds(ids);
}
@Override
public PageResponse<DTO> page(DTO dto, int current, int limit) {
Entity entity = toEntity(dto);
Page<Entity> entityPage = lambdaQuery().setEntity(entity).page(Page.of(current, limit));
List<Entity> records = entityPage.getRecords();
if (CollectionUtils.isEmpty(records)) {
return PageResponse.empty();
}
List<DTO> dtoList = records.stream().map(this::toDTO).collect(Collectors.toList());
return PageResponseBuilder.of(dtoList, entityPage);
}
@Override
public Map<Long, DTO> grouping(List<Long> ids) {
if (CollectionUtils.isEmpty(ids)) {
return Collections.emptyMap();
}
List<Entity> entityList = lambdaQuery().in(Entity::getId, ids).list();
if (CollectionUtils.isEmpty(entityList)) {
return Collections.emptyMap();
}
return entityList.stream().collect(Collectors.toMap(Entity::getId, this::toDTO));
}
public abstract DTO toDTO(Entity entity);
public abstract Entity toEntity(DTO dto);
}
- BaseServiceImpl类有三个泛型,分别是Mapper、Entity、DTO,这里需要子类声明这三个泛型
- BaseServiceImpl是实现BaseRemote的接口,对外承接DTO,然后转成数据库的实体类操作
- 有两个抽象方法,需要继承的子类实现toDTO和toEntity
- toDTO: 将实体类转成DTO
- toEntity: 将DTO转成实体类
具体子类实现
CarRemote
import com.nijia.zc.business.fp.dto.CarDTO;
import java.util.List;
/**
* @author fangxi created by 2021/12/13
*/
public interface CarRemote extends BaseRemote<CarDTO> {
}
CarServiceImpl
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import com.nijia.commons.dto.PageResponse;
import com.nijia.commons.dto.PageResponseBuilder;
import com.nijia.commons.utils.BeanUtils;
import com.nijia.commons.utils.CollectionUtils;
import com.nijia.zc.business.fp.dto.CarDTO;
import com.nijia.zc.business.fp.dto.CarImageDTO;
import com.nijia.zc.business.fp.entity.Car;
import com.nijia.zc.business.fp.mapper.CarMapper;
import com.nijia.zc.business.fp.remote.CarImageRemote;
import com.nijia.zc.business.fp.remote.CarRemote;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author fangxi created by 2021/12/13
*/
@Service
@DubboService
@RequiredArgsConstructor
public class CarServiceImpl extends BaseServiceImpl<CarMapper, Car, CarDTO> implements CarRemote {
@Override
public CarDTO toDTO(Car entity) {
return BeanUtils.copyProperties(entity, CarDTO.class);
}
@Override
public Car toEntity(CarDTO dto) {
return BeanUtils.copyProperties(dto, Car.class);
}
}
这里仅实现两个抽象方法即可。
这样,服务提供者已经提供了基本的CRUD实现,下面看看服务消费者如何实现
RPC服务消费者抽象实现
在服务消费者端,我们也需要一个接口和一个实现类进行对刚刚暴露的基本CRUD进行消费,他们分别是BaseService和BaseServiceImpl(和提供者BaseServiceImpl不冲突,因为这里是消费者,属于两个工程)
BaseService
import com.nijia.commons.dto.PageResponse;
import com.nijia.zc.business.fp.dto.BaseDTO;
import java.util.List;
import java.util.Map;
/**
* @author fangxi created by 2021/12/9
*/
public interface BaseService<DTO extends BaseDTO> {
DTO get(Long id);
List<DTO> selectAll();
boolean save(DTO dto);
boolean save(List<DTO> dtoList);
boolean update(DTO dto);
boolean update(List<DTO> dtoList);
boolean remove(Long id);
boolean remove(List<Long> ids);
PageResponse<DTO> page(DTO dto, int current, int limit);
Map<Long, DTO> grouping(List<Long> ids);
}
这个很简单,只是将BaseRemote的代码复制过来即可
BaseServiceImpl
import com.nijia.commons.dto.PageResponse;
import com.nijia.zc.business.fp.dto.BaseDTO;
import com.nijia.zc.business.fp.remote.BaseRemote;
import java.util.List;
import java.util.Map;
/**
* @author fangxi created by 2021/12/9
*/
public abstract class BaseServiceImpl<DTO extends BaseDTO> implements BaseService<DTO> {
public DTO get(Long id) {
return getBaseRemote().get(id);
}
public List<DTO> selectAll() {
return getBaseRemote().selectAll();
}
public boolean save(DTO dto) {
return getBaseRemote().save(dto);
}
public boolean save(List<DTO> dtoList) {
return getBaseRemote().save(dtoList);
}
public boolean update(DTO dto) {
return getBaseRemote().update(dto);
}
public boolean update(List<DTO> dtoList) {
return getBaseRemote().update(dtoList);
}
public boolean remove(Long id) {
return getBaseRemote().remove(id);
}
public boolean remove(List<Long> ids) {
return getBaseRemote().remove(ids);
}
public PageResponse<DTO> page(DTO dto, int current, int limit) {
return getBaseRemote().page(dto, current, limit);
}
public Map<Long, DTO> grouping(List<Long> ids) {
return getBaseRemote().grouping(ids);
}
public abstract BaseRemote<DTO> getBaseRemote();
}
这里需要写一个抽象方法getBaseRemote
,因为父类不知道具体是哪个服务消费,需要子类提供。
具体子类实现
CarService
/**
* @author fangxi created by 2021/12/13
*/
public interface CarService extends BaseService<CarDTO> {
}
CarServiceImpl
import com.nijia.cms.base.BaseServiceImpl;
import com.nijia.cms.component.JimiService;
import com.nijia.cms.pojo.dto.jimi.CurrentLocationDTO;
import com.nijia.cms.service.base.CarService;
import com.nijia.zc.business.fp.dto.CarDTO;
import com.nijia.zc.business.fp.remote.BaseRemote;
import com.nijia.zc.business.fp.remote.CarRemote;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
/**
* @author fangxi created by 2021/12/13
*/
@Service
@RequiredArgsConstructor
public class CarServiceImpl extends BaseServiceImpl<CarDTO> implements CarService {
@DubboReference
private CarRemote carRemote;
@Override
public BaseRemote<CarDTO> getBaseRemote() {
return this.carRemote;
}
}
将CarRemote注入进来,并通过getBaseRemote返回,让父类调用。
抽象接口暴露
至此,RPC服务提供者和RPC服务消费者都开发完成,已经可以实现基本的CURD代码进行RPC通信了。接下来还差最后一步,将基本的CURD代码暴露成Rest接口,已供前端调用。
新建一个BaseController
import com.nijia.commons.dto.PageRequest;
import com.nijia.commons.dto.PageResponse;
import com.nijia.commons.result.Result;
import com.nijia.zc.business.fp.dto.BaseDTO;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author fangxi created by 2021/12/9
*/
public abstract class BaseController<DTO extends BaseDTO, Service extends BaseService<DTO>> {
/**
* The Base service.
*/
@Autowired
protected Service baseService;
@ApiOperation("根据id查询")
@GetMapping("/{id:\\d+}")
public Result<DTO> get(@PathVariable Long id) {
DTO dto = baseService.get(id);
return Result.ok(dto);
}
@ApiOperation("查询全部")
@GetMapping("/all")
public Result<List<DTO>> selectAll() {
List<DTO> dtoList = baseService.selectAll();
return Result.ok(dtoList);
}
@ApiOperation("新增")
@PostMapping("/save")
public Result<Boolean> save(@RequestBody DTO dto) {
boolean save = baseService.save(dto);
return Result.ok(save);
}
@ApiOperation("批量新增")
@PostMapping("/save/batch")
public Result<Boolean> saveBatch(@RequestBody List<DTO> dtoList) {
boolean save = baseService.save(dtoList);
return Result.ok(save);
}
@ApiOperation("更新")
@PostMapping("/update")
public Result<Boolean> update(@RequestBody DTO dto) {
boolean update = baseService.update(dto);
return Result.ok(update);
}
@ApiOperation("批量新增")
@PostMapping("/update/batch")
public Result<Boolean> updateBatch(@RequestBody List<DTO> dtoList) {
boolean update = baseService.update(dtoList);
return Result.ok(update);
}
@ApiOperation("根据id删除")
@DeleteMapping("/remove/{id:\\d+}")
public Result<Boolean> remove(@PathVariable Long id) {
boolean remove = baseService.remove(id);
return Result.ok(remove);
}
@ApiOperation("根据id删除")
@DeleteMapping("/remove/batch")
public Result<Boolean> removeBatch(@RequestParam List<Long> ids) {
boolean remove = baseService.remove(ids);
return Result.ok(remove);
}
@ApiOperation("分页查询")
@GetMapping("/page")
public Result<PageResponse<DTO>> page(DTO dto, PageRequest pageRequest) {
PageResponse<DTO> pageResponse = baseService.page(dto, pageRequest.getCurrent(), pageRequest.getLimit());
return Result.ok(pageResponse);
}
}
这里的Result
和PageResponse
由自己实现特定的统一返回对象。
具体子类实现
import com.nijia.cms.base.BaseController;
import com.nijia.cms.pojo.dto.jimi.CurrentLocationDTO;
import com.nijia.cms.pojo.vo.CarVO;
import com.nijia.cms.service.base.CarService;
import com.nijia.commons.result.Result;
import com.nijia.commons.utils.BeanUtils;
import com.nijia.commons.utils.CollectionUtils;
import com.nijia.zc.business.fp.dto.CarDTO;
import com.nijia.zc.business.fp.dto.CarImageDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author fangxi created by 2021/12/13
*/
@Api(tags = "基础:车辆管理")
@RestController
@RequestMapping("/api/car")
@RequiredArgsConstructor
public class CarController extends BaseController<CarDTO, CarService> {
}
这样,CarController
不需要写任何代码,就已经实现了BaseController
中的所有接口。