2.4 商户注册

2.4.4 MapStruct对象转换

2.4.4.2 MapStruct

1、数据传输对象转换的繁琐

每层有自己的数据传输对象,当数据流转到该层又需要将数据转成符合要求的格式,比如:当数据由应用层流转到服务层则需要将数据转成DTO格式,当数据由服务层流向持久层则需要将数据转成Entity格式数据

下边的代码数据由服务层流向持久层:

@Override
public MerchantDTO createMerchant(MerchantDTO merchantDTO) {
    Merchant merchant = new Merchant();
    //设置审核状态0‐未申请,1‐已申请待审核,2‐审核通过,3‐审核拒绝
    merchant.setAuditStatus("0");
    //设置手机号
    merchant.setMobile(merchantDTO.getMobile());
    //...
    //保存商户
    merchantMapper.insert(merchant);
    //将新增商户id返回
    merchantDTO.setId(merchant.getId());
    return merchantDTO;
}

上边代码的问题是:由merchantDTO转成entity实现过程繁琐。

2、MapStruct解决数据传输对象转换的繁琐

MapStruct是一个代码生成器,它基于约定优于配置的方法大大简化了Java Bean对象之间的映射转换的实现。

MapStruct 使用简单的方法即可完成对象之间的转换,它速度快、类型安全且易于理解。

官方地址:https://mapstruct.org/

1)添加依赖

在使用MapStruct的工程中添加MapStruct依赖:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${org.mapstruct.version}</version>
</dependency>

2)服务层对象转换

在商户服务实现工程中定义商户对象转换类

定义MerchantConvert转换类,使用@Mapper注解快速实现对象转换

package com.huiminpay.merchant.convert;

import com.huiminpay.merchant.entity.Merchant;
import com.yh.merchant.api.dto.MerchantDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface MerchantCovert {
    MerchantCovert INSTANCE = Mappers.getMapper(MerchantCovert.class);
    MerchantDTO entity2Dto(Merchant merchant);
    Merchant dto2Entity(MerchantDTO dto);
}

在MerchantCovert中定义测试方法:

//dto转entity
MerchantDTO merchantDTO = new MerchantDTO();
merchantDTO.setUsername("测试");
merchantDTO.setPassword("111");
Merchant entity = MerchantCovert.INSTANCE.dto2entity(merchantDTO);

//entity转dto
entity.setMobile("123444554");
MerchantDTO merchantDTO1 = MerchantCovert.INSTANCE.entity2dto(entity);
System.out.println(merchantDTO1);

List数据也可以转换:

在MerchantCovert中定义list的方法,如下:

//list之间的转换   
List<MerchantDTO> listentity2dto(List<Merchant> list);

测试:

在main方法编写list之间的转换测试

//测试list之间的转换      
List<Merchant> list_entity = new ArrayList<>();      
list_entity.add(entity);      
List<MerchantDTO> erchantDTOS = MerchantCovert.INSTANCE.listentity2dto(list_entity); 
System.out.println(merchantDTOS);

3)应用层对象转换

在商户平台应用工程定义商户对象转换类

package com.huiminpay.merchant.convert;

import com.huiminpay.merchant.vo.MerchantRegisterVo;
import com.yh.merchant.api.dto.MerchantDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface MerchantRegisterConvert {
    MerchantRegisterConvert INSTANCE = Mappers.getMapper(MerchantRegisterConvert.class);

    MerchantDTO vo2dto(MerchantRegisterVo vo);

    MerchantRegisterVo dto2vo(MerchantDTO dto);
}

2.4.4.3 代码优化

1、优化服务层代码

修改商户服务工程 MerchantServiceImpl 中的 createMerchant 方法:

@Override
public MerchantDTO createMerchant(MerchantDTO merchantDTO) {
    Merchant merchant = MerchantCovert.INSTANCE.dto2Entity(merchantDTO);
    //设置审核状态0‐未申请,1‐已申请待审核,2‐审核通过,3‐审核拒绝
    merchant.setAuditStatus("0");
    //保存商户
    merchantMapper.insert(merchant);
    return MerchantCovert.INSTANCE.entity2Dto(merchant);
}

2、代码应用层代码

修改商户平台应用工程 MerchantController 中的 registerMerchant 方法:

@ApiOperation("注册商户")
@ApiImplicitParam(name = "merchantRegister", value = "注册信息", required = true, dataType =
        "MerchantRegisterVo", paramType = "body")
@PostMapping("/merchants/register")
public MerchantRegisterVo registerMerchant(@RequestBody MerchantRegisterVo merchantRegister) {
    //校验验证码
    smsService.checkVerifiyCode(merchantRegister.getVerifiykey(),
            merchantRegister.getVerifiyCode());
    //注册商户
    MerchantDTO merchantDTO = MerchantRegisterConvert.INSTANCE.vo2dto(merchantRegister);
    merchantService.createMerchant(merchantDTO);
    return merchantRegister;
}

2.4.4.4 注意MapStruct版本问题

因工程内使用的 Swagger 依赖 MapStruct (其版本低,与lombok同时使用会出现找不到实现类异常),

解决办法如下,可在common工程内引用版本,并排除 swagger 的 MapStruct 依赖

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-jdk8</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${org.mapstruct.version}</version>
</dependency>

2.4.5 异常处理

2.4.5.1 异常信息格式

系统在交互中难免会有异常发生,前端为了解析异常信息给用户提示,特定义了异常信息的返回格式,如下:

1、返回response状态说明

状态码

说明

200

成功

401

没有权限

500

程序错误(需要自定义错误体)

2、自定义错误体

{
    "errCode": "000000", 
    "errMessage": "错误说明" 
}

2.4.5.2 异常处理流程

截至目前系统并没有按照前端要求返回异常信息,测试如下: 注册商户时输入一个错误的验证码,返回信息如下:

{
  "timestamp": "2021-09-11T02:46:41.529+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "验证码已过期或者不存在",
  "path": "/merchant/merchants/register"
}

从上边的返回信息得知,状态码为500符合要求,按前端的规范定义的错误信息要写在 “errMessage” 中,显然不符合要求。

系统规范了异常处理流程,如下:

1、在服务层抛出自定义异常类型及不可预知异常类型。

上图中BusinessException为系统的自定义异常类型,程序中在代码显式抛出该异常,此类异常是程序员可预知的。

另一部分是系统无法预知的异常,如:数据库无法连接,服务器宕机等场景下所抛出的异常,此类异常是程序员无 法预知的异常。

2、应用层接收到服务层抛出异常继续向上抛出,应用层自己也可以抛出自定义异常类型及不可预知异常类型。

3、统一异常处理器捕获到异常进行解析。

判断如果为自定义异常则直接取出错误代码及错误信息,因为程序员在抛出自定义异常时已将错误代码和异常信息指定。

如果为不可预知的异常则统一定义为99999异常代码。

4、统一异常处理器将异常信息格式为前端要求的格式响应给前端。服务端统一将异常信息封装在下边的Json格式中返回:

{
    "errCode":"000000", 
    "errMessage": "错误说明"
}

2.4.5.3 自定义业务异常类

  1. 在 huiminpay-common 工程的 com.huiminpay.common.domain 包下添加业务异常类 BusinessException:
package com.huiminpay.common.cache.domain;

public class BusinessException extends RuntimeException {
    //错误代码
    private ErrorCode errorCode;

    public BusinessException(ErrorCode errorCode) {
        super();
        this.errorCode = errorCode;
    }

    public BusinessException() {
        super();
    }

    public void setErrorCode(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }
}
  1. 定义错误代码
    在 common工程专门定义了 ErrorCode 接口及 CommonErrorCode 通用代码。
    从资料文件夹的代码目录获取 CommonErrorCode.java 类。

2.4.5.4 自定义业务异常处理器

1、在huiminpay‐common 工程的 com.huiminpay.common.domain 包下添加错误响应包装类 RestErrorResponse:

package com.huiminpay.common.cache.domain;

import io.swagger.annotations.ApiModel;
import lombok.Data;

/**
 * @author Administrator
 * @version 1.0
 **/
@ApiModel(value = "RestErrorResponse", description = "错误响应参数包装")
@Data
public class RestErrorResponse {

    private String errCode;
    private String errMessage;

    public RestErrorResponse(String errCode, String errMessage){
        this.errCode = errCode;
        this.errMessage= errMessage;
    }
}

2、定义全局异常处理器

全局异常处理器使用 ControllerAdvice 注解实现,ControllerAdvice是SpringMVC3.2提供的注解,用ControllerAdvice可以方便实现对Controller面向切面编程,具体用法如下:

1、ControllerAdvice和ExceptionHandler注解实现全局异常处理

2、ControllerAdvice和ModelAttribute注解实现全局数据绑定

3、ControllerAdvice和InitBinder注解实现全局数据预处理

今天学习第一种用法,其它用法有兴趣的可自行查阅相关资料。

ControllerAdvice和ExceptionHandler结合可以捕获Controller抛出的异常,根据异常处理流程,Service和持久层最终都会抛给Controller,所以此方案可以实现全局异常捕获,异常被捕获到即可格式化为前端要的信息格式响应给前端。

huiminpay‐merchant‐application 工程的 com.huiminpay.merchant.common.intercept 添加

GlobalExceptionHandler:

package com.huiminpay.merchant.common.intercept;

import com.huiminpay.common.cache.domain.BusinessException;
import com.huiminpay.common.cache.domain.CommonErrorCode;
import com.huiminpay.common.cache.domain.ErrorCode;
import com.huiminpay.common.cache.domain.RestErrorResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 全局异常处理器
 * @author Administrator
 * @version 1.0
 **/
@ControllerAdvice//与@Exceptionhandler配合使用实现全局异常处理
public class GlobalExceptionHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //捕获Exception异常
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public RestErrorResponse processExcetion( HttpServletRequest request,
                                               HttpServletResponse response,
                                              Exception e){
        //解析异常信息
        //如果是系统自定义异常,直接取出errCode和errMessage
        if(e instanceof BusinessException){
            LOGGER.info(e.getMessage(),e);
            //解析系统自定义异常信息
            BusinessException businessException= (BusinessException) e;
            ErrorCode errorCode = businessException.getErrorCode();
            //错误代码
            int code = errorCode.getCode();
            //错误信息
            String desc = errorCode.getDesc();
            return new RestErrorResponse(String.valueOf(code),desc);
        }
        LOGGER.error("系统异常:",e);
        //统一定义为99999系统未知错误
        return new RestErrorResponse(String.valueOf(CommonErrorCode.UNKOWN.getCode()),CommonErrorCode.UNKOWN.getDesc());
    }
}

2.4.5.5 抛出自定义异常

按照异常处理流程,应用层抛出自定义异常由异常处理器进行解析。

1、校验验证码接口抛出 BusinessException

修改商户平台应用工程中SmsServicer的verificationMessageCode接口

public void checkVerifyCode(String verifyKey, String verifyCode) throws BusinessException;

接口实现中抛出异常自定义异常类型

@Override
public void checkVerifiyCode(String verifiyKey, String verifiyCode) {
    String url = smsUrl + "/verify?name=sms&verificatinotallow=" + verifiyCode + "&verificatinotallow=" + verifiyKey;
    Map responseMap = null;
    try {
        //请求校验验证码
        ResponseEntity<Map> exchange = restTemplate.exchange(url, HttpMethod.POST,
                HttpEntity.EMPTY, Map.class);
        responseMap = exchange.getBody();
        log.info("校验验证码,响应内容:{}", JSON.toJSONString(responseMap));
    } catch (RestClientException e) {
        e.printStackTrace();
        log.info(e.getMessage(), e);
        throw new BusinessException(CommonErrorCode.E_100102);
    }
    if (responseMap == null || responseMap.get("result") == null || !(Boolean)responseMap.get("result")) {
        throw new BusinessException(CommonErrorCode.E_100102);
    }
}

2、测试

请求商户注册,输出一个错误的验证码,返回信息如下

{
  "errCode": "100102",
  "errMessage": "验证码错误"
}

3、测试不可预知异常

故意在Controller中制造异常,测试是否抛出未知错误异常。 代码如下:

@PostMapping("/merchants/register")      
public MerchantRegisterVO registerMerchant(@RequestBody MerchantRegisterVO merchantRegister){ 
    int i=1/0;//故意制造异常
    .... 
}

请商户注册,返回信息如下:

{
  "errCode": "999999",
  "errMessage": "未知错误"
}

2.4.6 校验商户手机号

2.4.6.1 实现思路

校验商户手机号的唯一性,根据商户的手机号查询商户表,

如果存在记录则说明已有相同的手机号重复,手机号不唯一则抛出自定义异常。

2.4.6.2 完善代码

1、修改商户服务-注册商户接口,添加抛出异常声明

MerchantDTO createMerchant(MerchantDTO merchantDTO) throws BusinessException;

2、修改商户服务-注册商户接口实现方法

if (merchantDTO == null) {
    throw new BusinessException(CommonErrorCode.E_100108);
}
//手机号非空校验
if (StringUtils.isEmpty(merchantDTO.getMobile())) {
    throw new BusinessException(CommonErrorCode.E_100112);
}
//手机号合法性校验
if (!PhoneUtil.isMatches(merchantDTO.getMobile())) {
    throw new BusinessException(CommonErrorCode.E_100109);
}
//联系人非空校验
if (StringUtils.isBlank(merchantDTO.getUsername())) {
    throw new BusinessException(CommonErrorCode.E_100110);
}
//密码非空校验
if (StringUtils.isBlank(merchantDTO.getPassword())) {
    throw new BusinessException(CommonErrorCode.E_100111);
}
LambdaQueryWrapper<Merchant> lqw = new LambdaQueryWrapper<>();
lqw.eq(Merchant::getMobile,merchantDTO.getMobile());
Integer count  = merchantMapper.selectCount(lqw);
if(count>0){
    throw  new BusinessException(CommonErrorCode.E_100113);
}

3、修改商户应用平台-注册商户接口添加对注册信息的非空校验。

// 1.校验
if (merchantRegister == null) {
    throw new BusinessException(CommonErrorCode.E_100108);
}
//手机号非空校验
if (StringUtils.isBlank(merchantRegister.getMobile())) {
    throw new BusinessException(CommonErrorCode.E_100112);
}
//校验手机号的合法性
if (!PhoneUtil.isMatches(merchantRegister.getMobile())) {
    throw new BusinessException(CommonErrorCode.E_100109);
}
//联系人非空校验
if (StringUtils.isBlank(merchantRegister.getUsername())) {
    throw new BusinessException(CommonErrorCode.E_100110);
}
//密码非空校验
if (StringUtils.isBlank(merchantRegister.getPassword())) {
    throw new BusinessException(CommonErrorCode.E_100111);
}
//验证码非空校验
if (StringUtils.isBlank(merchantRegister.getVerifiyCode()) ||
        StringUtils.isBlank(merchantRegister.getVerifiykey())) {
    throw new BusinessException(CommonErrorCode.E_100103);
}