基于RPC框架的通用代码实现


基于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:BaseDTOCarDTO

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);
    }

}

这里的ResultPageResponse由自己实现特定的统一返回对象。

具体子类实现

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中的所有接口。


Author: Re:0
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Re:0 !
  TOC