跳至主要內容

项目开发规范

杨轩-国实信息大约 17 分钟规范

开发环境

采用JDK1.8,代码采用Lambda风格编码。

项目包名

在Java编程规范中,要求开发人员在自己定义的类名前加上唯一的前缀。由于互联网上的域名称是不会重复的,很多开发人员采用自己公司在互联网上的域名称作为自己程序包的唯一前缀。

一般公司命名为 com.公司名.组织名.项目名.模块名.XXX,项目中的包名都由小写单词组成。

例如:com.gosci.tech.engine.cdm

模块名

项目业务模块名,例如:system、wms等

Module模块是项目中的要实现的一个业务模块。例如,system是主要实现系统相关的业务,wms主要实现仓储相关的业务。与项目包名组成了com.xx...xx.demo.xx的结构。

控制访问层

Controller 前端控制器,负责处理由DispatcherServlet分发的请求。

1.类注释

/**
 * @author: 杨轩-国实信息
 * @date: 2020-03-22 20:35:25
 * @desc: 描述类的作用
 */
  • @author :创建人
  • @date :创建间
  • @desc:功能描述

2.类注解

使用@RestController代替@Controller@ResponseBody两个注解。

使用@Slf4j,打印日志注解。用来代替如下代码:

private final Logger log = LoggerFactory.getLogger(当前类名.class);

使用log.info()log.error()等方法记录日志。

使用@Api,使用Swagger工具对类的描述。使用方式@Api(tags=" ", description=" ")

使用@AllArgsConstructor@RequiredArgsConstructor,相当于添加一个构造函数,该构造函数含有所有已声明字段属性参数。这个注解在引用Service时使用, 通常使用 @AllArgsConstructor ,特殊情况下使用@RequiredArgsConstructor,下面Service中会讲解。

3.引入Service

bean的注入不推荐使用@Autowired注入,项目中使用了lombok后,可以通过它的注解实现构造器注入,而且代码比较清晰,易理解。

@AllArgsConstructor

在实体类上增加了 @AllArgsConstructor,可以通过如下引入实体类:

private final IDemoService demoService;

@AllArgsContructor会生成一个包含所有变量的构造方法,相当于以下代码:

private IDemoService demoService;

public DemoController(IDemoService demoService){
  this.demoService = demoService
}

@RequiredArgsConstructor

在什么时候使用@RequiredArgsConstructor呢?如下代码:

private final IDemoService demoService; 接口实现
private static final String PREFIX = "/demo";  常量

在以上这种情况下一个DemoController中既引用了Service,也存在自己的常量的情况下,使用@RequiredArgsConstructor

@RequiredArgsConstructor会生成一个包含标识了final,和标识了@NonNull(lombok自己的注解,不是valid的)的变量的构造方法,生成的构造方法是私有的private。

4.返回数据方法

参考代码如下:

@ApiOperation(value = "获得对象信息通过id", notes = "")
@ApiResponses(@ApiResponse(code = 200, message = "处理成功", response = DataBrand.class))
@GetMapping(value="/getById/{id}")
public Result<DataBrandVO> getById(
        HttpServletRequest request, 
        HttpServletResponse response, 
        @PathVariable("id") Integer id) {
    if(ObjectUtil.isNull(id)){
        throw new InvalidRequestException(ResultFail.PARAM_MISS_ERROR);
    }
    DataBrand dataBrand = dataBrandService.getById(id);
    DataBrandVO dataBrandVo = new DataBrandVO();
    BeanUtil.copyProperties(dataBrand, dataBrandVo);
    return Result.ok(dataBrandVo);
}

数据库映射的实体类不允许直接返回,返回的参数需要按需返回。比如前端需要返回name、code、note三个属性,那么在DataBrandVO中只能出现这三个属性,代码生成器会基于数据库生成一个通用的Vo,需要根据这个默认的Vo进行一下处理。

5.Swagger方法注解

@ApiOperation@ApiImplicitParams@ApiResponses等属于Swagger的注解需要加上。

6.SpringMvc的请求协议

@GetMapping一样的还有@PostMapping@PutMapping, @DeleteMapping,下面我们来看看这四个的使用场景。

RestFul的HTTP方法有很多,比如:GET、HEAD、POST、PUT、DELETE、OPTIONS、TRACE、PATCH,我们主要用到GET、POST、PUT、DELETE。

RestFul API通用规范

  • @GetMapping 对应的是GET方法,用来获取资源,安全且幂等。例如:/getById/{id}/download/idget(T entity)list(T entity)。我们可以理解为Get使用是用/url/{xxx}方式来获取数据。
  • @PostMapping 对应的是POST方法,用在创建新的数据、获取资源、复杂逻辑功能、上传功能,也可以用于更新数据优先级使用低于PUT方法,不安全且不幂等。例如:save(T entity)。我们可以理解为POST请求方式是/url,没有指定明确的URL路径参数,被请求的参数获取全靠服务器内部请求对象等方式去获取。
  • @PutMapping 对应的是PUT方法,本质上来讲, PUT和POST极为相似,都是向服务器发送数据。但它们之间有一个重要区别,PUT通常指定了资源的存放位置,而POST则没有,POST的数据存放位置由服务器自己决定,不安全但幂等。例如/updateById/{id}
  • @DeleteMapping, 对应的是DELETE方法,就是用来删除某一个资源的。

HTTP协议的关键特点安全性冥等性,综上所述,我们在实际使用中如下:

  • 查询工作。请求路径上存在参数的,通过@PathVariable解析的查询使用@GetMapping
  • 查询工作,请求路径上不存在参数的,参数解析通过@RequestParam的查询请求采用@GetMapping
  • 查询工作,请求路径上不存在参数的,参数长度小于1024字节的用@GetMapping否则用@PostMapping
  • 创建、更新、删除工作,@PostMapping目前所有的提交工作都可以用这个完成。
  • 不符合以上@PutMapping@DeleteMapping使用说明的用@PostMapping

注意

但是,实际项目中不推荐使用@PathVariable,因为现在在权限系统中使用Path变量时不利于权限控制。

/user/info/{userId},就是不好的示例。

7.权限注解的定义与使用

关于在Controller方法上加的权限注解,可优先使用SpringSecurity自带的权限注解@PreAuthorize。因为很多单体框架中使用的是SpringSecurity,所以优先采用这种方式。

权限注解的意义是,在我们需要响应一个客户端的请求前,首先需要确定下当前客户端的请求是否在授权范围内,在的话准许请求,不在的话请求403受限。注解主要与页面上的按钮搭配使用。

@PreAuthorize是SpringSecurity提供的进行访问控制注解。可以用来控制一个方法是否能够被调用。主要使用方式是:

  • @PreAuthorize("hasRole('ROLE_USER'))
  • @PreAuthorize("hasAuthority('ROLE_USER'))
  • @PreAuthorize("#Spel表达式)")

如果项目中没有使用SpringSecurity的话,也可以使用SpringAOP原理自定义的权限注解。主要应用于前后端分离框架和分布式框架,权限控制方式是采用的SpringSecurity+Oauth2的无状态Tokens方式来管理权限控制。

8.获取参数的方式使用场景

Controller中的传参方式主要有,@PathVariable,@RequestParam,@ModelAttribute,@RequestBody

@PathVariable,映射 URL 绑定的占位符, 即 url/{paramId}, 这时的paramId可通过 @Pathvariable注解绑定它传过来的值到方法的参数上。

示例代码:

@Controller  
@RequestMapping("/owners")  
public class RelativePathUriTemplateController {   
  @RequestMapping("/pets/{petId}")  
  public void findPet(@PathVariable String petId) {      
    // implementation omitted   
  }  
}

访问方式:http://ip:port/owners/pets/参数petId接收参数。

@RequestParam用来处理简单类型的绑定,示例代码:

@Controller  
@RequestMapping("/pets")  
public class EditPetForm {  
	@GetMapping
	public String loadPet(@RequestParam("petId") int petId) {  
		Pet pet = clinic.loadPet(petId);  
		return "success";  
	}
}

@RequestBody注解常用来处理application/jsonapplication/xml等编码的参数。

@ModelAttribute注解有两个用法,一个是用于方法上,一个是用于参数上,这里主要说用于参数上的场景。

用于参数上时,用来通过名称对应,把相应名称的值绑定到注解的参数bean上,示例代码如下:

@PostMapping(value="/owners/{ownerId}/pets/{petId}/edit")  
public String processSubmit(@ModelAttribute Pet pet) {  
	//...
}

9.方法的返回值

关于Controller有三种常见返回类型,StringResult<T>T,确切的说再加上异常throw new JsonException(" ")是四种。

  • String:基本已弃用,在传统springMVC单体框架中使用,用于返回请求路径
  • T:返回对象或者对象集合,如果要在接口上传输建议加上Result<T>
  • Result<T>:在Controller框架中的增、删、改的返回结果中使用,Result已在framwork-api包中封装。

示例代码:

public Result<Void> update(
        HttpServletRequest request,
        HttpServletResponse response,
        @ModelAttribute PurchaseOrderWithBranchsDTO purchaseOrderDTO) {
    if(ObjectUtil.isNull(purchaseOrderDTO.getId())) {
        throw new JsonException(CommonErrorCode.PARAM_MISS);
    }
    PurchaseOrder purchaseOrderOf = purchaseOrderService.getById(purchaseOrderDTO.getId());
    if(!StrUtil.equals(purchaseOrderDTO.getStatus(), purchaseOrderOf.getStatus())){
        throw new JsonException("采购单状态已变更,不能修改!");
    }
    BeanUtil.copyProperties(purchaseOrderOf, purchaseOrderDTO, CopyOptions.create().ignoreNullValue());
    BeanUtil.copyProperties(purchaseOrderDTO, purchaseOrderOf);
    ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(purchaseOrderOf);
    if(validResult.hasErrors()){
        log.error("提交数据格式不合规则【"+validResult.getErrors()+"】。");
        throw new JsonException(CommonErrorCode.PARAM_VALID_ERROR);
    }
    try {
        purchaseOrderOf.setPurchaseOrderBranchList(
                purchaseOrder.removeOrderBranchEmptyData(purchaseOrder.getPurchaseOrderBranchList()));
        purchaseOrderService.updatePurchaseOrderAndBranchList(purchaseOrderOf);
        return Result.ok();
    } catch (Exception e) {
        if(log.isDebugEnabled()){
            e.printStackTrace();
        }
        throw new JsonException(CommonErrorCode.DATA_UPDATE_FAILED);
    }
}

写Controller的方法中不推荐多重if()else()循环写法,代码如下:

if(***){
    if(***){
        if(***){
         ---代码   
        } else {
            return 
        } 
    } else {
        return 
    }    
} else {
    return 
}

推荐代码,如下:

if(***){  //判断不符合逻辑
  throw 异常
}
//正常逻辑代码   

throw new InvalidRequestException(ResultFail.xxx),异常类。

可以实现一个ResultFail类,用来定义常用的异常枚举。

try {
	---代码---
	return Result.ok(); //或者return Result.ok("成功提示");
}catch(Exception ex){   
	if(log.isDebugEnabled()){
		e.printStackTrace();
	}
	throw new InvalidRequestException(ResultFail.DATA_UPDATE_FAILED);
}

Result.ok();默认的的成功提示ResultStatus.SUCCESS,如果有特殊要求可以自定义成功提示。

通常在controller中不使用catch捕获异常,抛出的异常直接由@RestControllerAdvice捕获并返回给前端。

10.请求的参数

参考代码如下:

@ApiOperation(value = "获得对象信息通过id", notes = "")
@ApiResponses(@ApiResponse(code = 200, message = "处理成功", response = DataBrand.class))
@GetMapping(value="/getById/{id}")
public Result<DataBrandVO> getById(
        HttpServletRequest request, 
        HttpServletResponse response, 
        @RequestBody("dataBrandDto") DataBrandDto dataBrandDto) {
    if(!dataBrandDto.validateDto()){
        throw new InvalidRequestException(ResultFail.PARAM_MISS_ERROR);
    }
    ... ...
    return Result.ok(dataBrandVo);
}

DataBrandDto里的参数值,比如前端传了name、code两个参数,这个类里建议只包含name、code,可以根据代码生成的DataBrandEntity来复制和改造,另外类的属性上需要加上Hibernate Validate注解。

前面提到的方法的返回值的示例代码中ValidationUtil.ValidResult,做的是数据格式验证,这个主要采用了hibernate-validator实现,需要配合在实体类属性上的注解实现,讲实体类规范会详细讲解。

DataBrandDto代码实例:

@Data
@ApiModel(... ...)
public class DataBrandVo implements Serializable {
  
  @NotBlank(message="名称不能为空!")
  @ApiModelProperty("名称")
  private String name;
  
  @Length(max=32, message="编码最长32位")
  @ApiModelProperty("编码")
  private String code;
  
  public boolean validateDto(){
    return ValidationUtil.validated(this);
  }
}

11.日志注解

可以基于Spring Aop切面实现日志注解,在Controller类的方法上加注解,实现日志记录。

@Log("日志记录")
public Result<PurchaseOrder> update(){
}

12.上传与导出规范

注意

以下Excel导入导出可能由于文件过大导致系统OOM,使用阿里的EasyExcel,可以解决大文件内存溢出的问题。

上传

代码如下:

public Result<Void> save(
	HttpServletRequest request,
	HttpServletResponse response,
	@RequestParam(value = "file") MultipartFile file){
}

@RequestParam(value = "file") MultipartFile file,通过MultipartFile获取文件流然后解析,文件上传的配置可在yml中配置如下:

spring:
  servlet:
    multipart:
      enabled: true //是否使用
      max-file-size: 10MB //是单个文件大小
      max-request-size: 100MB //设置总上传的数据大小

导入

参考代码如下:

@PostMapping(value = "/importData")
public Result<String> importData(
        HttpServletRequest request,
        @RequestParam(value = "file") MultipartFile file) throws IOException {
    long beginTime = System.currentTimeMillis();
    String fileType = FileTypeUtil.getType(file.getInputStream());
    List<String> allowType = Arrays.asList("xls");
    if (!allowType.contains(fileType)) {
        throw new JsonException("请导入.xls后缀文件");
    }
    // 判断用户上传的文件,是否符合模板要求
    String[] titleArrs = new String[]{"xxx", ...};
    ExcelReader reader0 = null;
    List<List<Object>> list0 = null;

    reader0 = ExcelUtil.getReader(file.getInputStream(), 0);
    list0 = reader0.read();
    List<String> sheetNames = reader0.getSheetNames();
    if (!sheetNames.get(0).equals("sheet名称")) {
        throw new JsonException("sheet名称模版不正确");
    }
    List<String> titleList = list0.get(0).stream()
            .map(Object::toString).collect(Collectors.toList());
    for (int i = 0; i < titleArrs.length; i++) {
        if (!titleArrs[i].equals(titleList.get(i))) {
            throw new JsonException("请勿更改模板标题");
        }
    }
    if (list0.size() <= 1) {
        throw new JsonException("请勿导入空文件");
    }
    List<Simple> importData = Lists.newArrayList();
    List<String> o = Lists.newArrayList();
    Simple simple = null;
    for (int i = 1; i < list0.size(); i++) {
        simple = new Simple();
        o = list0.get(i).stream()
                .map(StrUtil::toString)
                .collect(Collectors.toList());
        employStation.setXXX(o.get(1).trim());
        ... ...
        importData.add(simple);
    }
    try {
        simpleService.importData(importData);
        long endTime = System.currentTimeMillis();
        String importMsg = "导入成功耗时:" + (beginTime - endTime) / 1000 + "秒";
        return Result.ok(importMsg);
    } catch (Exception e) {
        e.printStackTrace();
        return Result.fail(CommonErrorCode.DATA_ADD_FAILED);
    }
}

FileTypeUtilExcelReaderExcelUtil使用了Hutool工具类。

导出

参考代码如下:

@GetMapping(value = "/exportData")
public void exportData(
        HttpServletRequest request,
        HttpServletResponse response,
        @ModelAttribute Simple simple) {
    QueryWrapper<Simple> wrapper = new QueryWrapper<>();
    List<Simple> exportData = simpleService.list(wrapper);
    String[] sexNames = new String[]{"女","男"};
    exportData = exportData.stream().map(element->
            element.setEmpSex(sexNames[Convert.toInt(element.getEmpSex())]))
            .collect(Collectors.toList());
    Object[][] exportInfo = {
            {"xxx","标题",22}
    };
    ExcelWriter writer = ExcelUtil.getWriter();
    Map<String,String> headerMap = new LinkedHashMap<>();
    for (int i = 0; i < exportInfo.length; i++) {
        Object[] item = exportInfo[i];
        headerMap.put(item[0].toString(),item[1].toString());
        writer.setColumnWidth(i,Integer.parseInt(item[2].toString()));
    }
    Font font = writer.createFont();
    font.setFontHeightInPoints((short) 12);
    font.setFontName("宋体");

    StyleSet style = writer.getStyleSet();
    style.setAlign(HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
    style.setFont(font,false);

    writer.setHeaderAlias(headerMap);
    writer.setOnlyAlias(true);

    writer.write(exportData, true);
    response.setContentType("application/vnd.ms-excel;charset=utf-8");
    String fileName = DateUtil.format(LocalDateTime.now(),"yyyyMMddHHmmss")+".xls";
    response.setHeader("Content-Disposition", "attachment;filename="+fileName);
    ServletOutputStream out = null;
    try {
        out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(out);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

ExcelWriterExcelUtilIoUtil使用的是Hutool工具类。

实体类

可以直接用代码生成组件基于数据库表进行生成。

一个系统日志的实体类,示例代码如下:

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("system_log")
@ApiModel("系统日志表")
public class SystemLog implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键ID
     */
    @TableId(value = "ID", type = IdType.AUTO)
    @NotNull(message="主键ID不能为空!")
    @ApiModelProperty("主键")
    private Long id; 

    /**
     * 日志类型 0业务日志,1登陆日志
     */
    @TableField("TYPE")
    @NotBlank(message="日志类型不能为空!")
    @Length(min=1, max=1,message="日志类型长度必须介于1~1位字符间")
    @ApiModelProperty("日志类型")
    private String type; 

    /**
     * 日志标题
     */
    @TableField("TITLE")
    @NotBlank(message="日志标题不能为空!")
    @Length(min=1, max=50,message="日志标题长度必须介于1~50位字符间")
    @ApiModelProperty("日志标题")
    private String title; 

    /**
     * 日志IP
     */
    @TableField("REMOTE_ADDR")
    @Length(max=50,message="IP长度不超过50位字符")
    @ApiModelProperty("日志IP")
    private String remoteAddr; 

    /**
     * 用户名称
     */
    @TableField("USER_NAME")
    @Length(max=50,message="用户名称长度不超过200位字符")
    @NotBlank(message="用户名称不能为空!")
    @ApiModelProperty("用户名称")
    private String userName; 
    
    /**
     * 用户ID
     */
    @TableField("USER_ID")
    @ApiModelProperty("用户ID")
    private int userId; 

    /**
     * 日志链接
     */
    @TableField("REQUEST_URI")
    @Length(max=255,message="日志链接长度不超过255位字符")
    @ApiModelProperty("日志链接")
    private String requestUri; 

    /**
     * 方法
     */
    @TableField("METHOD")
    @Length(max=50,message="方法长度不超过50位字符")
    @ApiModelProperty("请求方法")
    private String method; 

    /**
     * 参数
     */
    @TableField("PARAMS")
    @ApiModelProperty("请求参数")
    private String params; 

    /**
     * 异常
     */
    @TableField("EXCEPTION")
    @ApiModelProperty("请求异常")
    private String exception; 
    
    /**
     * 租户ID
     */
    @TableField("TENANT_ID")
    @ApiModelProperty("租户ID")
    private Integer tenantId; 

    /**
     * 版本
     */
    @TableField("VERSION")
    @ApiModelProperty("version")
    @Version
    private Integer version; 

    /**
     * 创建时间
     */
    @TableField(value = "CREATE_DATE", fill = FieldFill.INSERT)
    @NotNull(message="创建时间不能为空!")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createDate; 
    
}

实体类注解:

  • @Data是Lombok注解,相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode
  • 使用@EqualsAndHashCode(callSuper = false) 因为是实现了Serializable,没有父级使用更改成false
  • @Accessors(chain = true),chain 若为true,则setter方法返回当前对象
  • @TableName("system_log"),@TableName为MybatisPlus的表映射
  • @ApiModel,Swagger在线文档的实体对象注解

实体类属性注解:

  • @TableId(value = "ID", type = IdType.AUTO),type主键注解,主键生成方式,fill = FieldFill.INSERT是新增填称还是更新填充
  • @NotNull非字符串判空注解、@NotBlank字符串判空注解
  • @ApiModelProperty,Swagger在线文档对象属性注解
  • @Length,允许输入的长度范围值
  • @JsonFormat,输出的时间格式
  • @DateTimeFormat,输入的时间格式
  • @Version采用的是Mybatisplus的版本

另外定义数据库数字定义属性类型为Integer或者Long,定义时间为LocalDate或者LocalDateTime,其他看情况而定。

实体类原则要求属性字段要与数据库字段保持一致。

模型类

模型类是在业务模块下建model的包,模型类共有三部分组成。

DTO、VO等

接收和输出的实体类,代码如下:

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel("工作岗位分页类")
public class EmployStationWithPage {

    @NotNull(message="当前页不能为空!")
    @ApiModelProperty("当前页")
    private Integer pageNo;

    @NotNull(message="每页显示数不能为空!")
    @ApiModelProperty("每页显示条数")
    private Integer pageSize;

    @ApiModelProperty("用户UserKey")
    private String userKey;

    @ApiModelProperty(value = "岗位名称")
    private String stationName;

    @ApiModelProperty(value = "单位名称")
    private String stationCompany;

    @ApiModelProperty(value = "工作地点")
    private String address;
    
    public boolean validateDto(){
      return ValidationUtil.validated(this);
    }
}

@Data、@EqualsAndHashCode、@Accessors、@ApiModel、@ApiModelProperty,属性设置参考实体类。

模型类不直接使用数据库实体类,作为传入和传出数据格式输出,与实体类交互需要通过BeanUtil进行数据拷贝。

枚举类

在model包下建enums,存放的是当前业务模块下存放的类似于123等属性自定义的枚举变量,代码如下:

/**
 * 投递状态
 */
@ToString
public enum DeliverFlagEnum {

    NOT("0", "否"),
    IS("1", "是");

    @Getter
    @Setter
    private String value;

    @Getter
    @Setter
    private String msg;

    DeliverFlagEnum(String value, String msg) {
        this.value = value;
        this.msg = msg;
    }
}

枚举类是enums类型,是对一组属性的定义。

常量包

在model建consts常量包,在常量包中存放业务模块全局的常量,优先级低于枚举包,代码如下:

public class SecurityContants {
	
	/**
	 * 超级管理员
	 */
	public static final String USER_ADMIN = "0"; 
	
	/**
	 * 允许登录
	 */
	public static final String ALLOW_LOGIN = "0";
}

常量类定义单个业务属性,属性使用大写,用public static final 修饰。

推荐使用另一种方式,直接定义为接口,因为接口中的属性天然被public static final 修饰。

public interface SecurityContants {
    String USER_ADMIN = "0"; 
    String ALLOW_LOGIN = "0";
}

服务接口及其服务实现类

对mybatis-plus进行了扩展,具体设计过程可以看mybatis-plus组件

定义服务接口

接口类即实现业务接口,代码如下:

//import com.baomidou.mybatisplus.extension.service.IService;
import com.gosci.tech.mybatisplus.base.BaseService;

public interface IEmployStationService extends BaseService<EmployStation> {

    /**
     * 导入工作岗位信息
     * @param employStationList
     */
    void importData(List<EmployStation> employStationList);

}

mybatis-plus类库提供的父级接口是com.baomidou.mybatisplus.extension.service.IService,我们需要写一个BaseService去扩展它,原因是原来的IService不支持实体类传参等,都需要被Wrapper包裹查询条件。

定义服务实现类

业务实现类代码,代码如下:

//import com.baomidou.mybatisplus.extension.service.ServiceImpl;
import com.gosci.tech.mybatisplus.base.BaseServiceImpl;

@Slf4j
@Service("employDeliverRecordService")
@Transactional(readOnly = true, rollbackFor = Exception.class)
@AllArgsConstructor
public class EmployDeliverRecordServiceImpl extends BaseServiceImpl<IEmployDeliverRecordDAO, 
        EmployDeliverRecord> implements IEmployDeliverRecordService {

    private final IEmployDeliverSnapshotDAO deliverSnapshotDAO;

    @Override
    public void deliverResume(EmployDeliverRecord deliverRecord, ResumeBasicInfo basicInfo) {
        //查询工作经历列表
        List<ResumeWorkHistory> workHistoryList = workHistoryDAO.selectList(
                new LambdaQueryWrapper<ResumeWorkHistory>()
                .eq(ResumeWorkHistory::getUserKey, deliverRecord.getUserKey()));
        if(ObjectUtil.isEmpty(workHistoryList)){
            log.info("... ...");
            throw new JsonException("... ....");
        }        
    }
    
    @Transactional( rollbackFor = Exception.class)
    @Override
    public void saveResume(EmployDeliverRecord deliverRecord) {
        //保存工作经历列表
        super.save(deliverRecord);  
    }
}
  • 实体类继承BaseServiceImpl
  • @Slf4j是lombok的日志注解
  • @Service("employDeliverRecordService")定义服务beanName
  • @Transactional(rollbackFor = Exception.class)事务注解
  • @AllArgsConstructor是Lombok的构造函数注解,匹配的Bean引入采用private final方式,不采用@Autowired方式
  • 非查询接口尽量返回void,如果有判断异常可以throw new BizException() 判处异常,由统一异常工具捕获
  • 非批量增删改操作,引入的持久层DAO而不是Service,同时能够避免service层之间的相互调用
  • 需要对每一个操作加上注释,必要时加上修改注释

持久层接口类和实现XML

持久层接口类

持久层类代码如下:

//import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gosci.tech.mybatisplus.base.BaseMapper;

/**
 * 工作岗位表 Mapper 接口
 */
@Mapper
public interface IEmployStationDAO extends BaseMapper<xxx> {
}

BaseMapper继承了mybatis-plus的原生BaseMapper,接口需要添加@Mapper注解。

持久层实现XML

Mapper.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.gosci.tech.archetype.auth.mapper.ILogMapper">

    <resultMap type="com.gosci.tech.archetype.auth.entity.Log" id="LogResult">
                    <result property="id" column="id"/>
                    <result property="dataType" column="data_type"/>
                    <result property="dataId" column="data_id"/>
                    <result property="operatorType" column="operator_type"/>
                    <result property="dataSource" column="data_source"/>
                    <result property="deviceId" column="device_id"/>
                    <result property="beforeDetail" column="before_detail"/>
                    <result property="afterDetail" column="after_detail"/>
                    <result property="operator" column="operator"/>
                    <result property="createTime" column="create_time"/>
            </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="BASE_COLUMN_LIST">
                id,
                data_type,
                data_id,
                operator_type,
                data_source,
                device_id,
                before_detail,
                after_detail,
                operator,
                create_time,
            </sql>

    <sql id="BASE_TABLE_NAME">
		log
	</sql>
</mapper>

mapper使用mybatis的原生mapper,简单业务采用mybatis-plus的条件构造器可以实现,复杂的可以写mapper,但是建议我们在设计业务表的时候尽量少用关联。

分布式Api规范

1.定义feign接口

feign接口代码类代码如下:

/**
 * 演示DEMO 接口
 */
@FeignClient(
        value = ShowcaseApiConstant.APPLICATION_NAME,
        fallbackFactory = ExampleTenantApiFallbackFactory.class,
        path = "/api/example/tenant"
)
public interface IExampleTenantApi {
    
    @PostMapping("/getTenantList")
    Result<List<ExampleTenantVO>> getTenantList(@RequestBody ExampleTenantDTO exampleTenantDTO);
}

@FeignClient是feign对外服务注解,ExampleTenantApiFallbackFactory是定义的熔断工厂类。

2.实现feign接口

feign接口代码类代码如下:

/**
 * 演示DEMO 接口实现
 */
@Slf4j
@ApiIgnore
@RestController
@RequestMapping(value="/api/example/tenant")
@AllArgsConstructor
public class ExampleTenantApi implements IExampleTenantApi {

    private final IExampleTenantService tenantService;

    @PostMapping("/getById")
    @Override
    public Result<List<ExampleTenantVO>> getTenantList(ExampleTenantDTO exampleTenantDTO) {
        List<ExampleTenantVO> exampleTenantVOList = Lists.newArrayList();
        ExampleTenantVO exampleTenantVO;
        ExampleTenant exampleTenant = new ExampleTenant();
        BeanUtil.copyProperties(exampleTenantDTO, exampleTenant);
        List<ExampleTenant> exampleTenantList = tenantService.list(exampleTenant);
        for (ExampleTenant tenant: exampleTenantList) {
            exampleTenantVO = new ExampleTenantVO();
            BeanUtil.copyProperties(tenant, exampleTenantVO);
            exampleTenantVOList.add(exampleTenantVO);
        }
        return Result.ok(exampleTenantVOList);
    }
}