近期项目搭建上的一些实践及思考

最近在参与公司内部的中台建设,同时启动了多个项目,因此形成一套统一的项目搭建思路及架构就变得比较迫切,这里将最近的一些成果,包括趟过的坑记录下来,汇总下。当然之后如果个人能力有所成长,肯定还有更多改动,拥抱变化嘛!

由于公司内部有一套还算成熟的rpc框架,也强制要求必须使用,就没有采用外部比较流行的dubbo、spring boot等开源框架,不过今天的重点不在这里,而在于项目内部的结构设计及一些公用实现,下面开始展开。

一个项目,如果需要对外提供服务,目前最简单的通用做法是创建两个bundle,一个负责具体的业务实现service-bundle,另外一个负责对外提供client-jar包,首先说下client bundle(我这边取名为service-api)

Service-Api bundle

先贴下我这边项目的对应结构:(忽略build及out目录,另外屏蔽掉了部分敏感包名信息哈)

近期项目搭建上的一些实践及思考_第1张图片
image

一个完备的client需要至少具备两方面的内容,常量&服务说明:

常量

对应上图中的constants包,其中定义了枚举及常量类,当然不是所有的枚举都合适放在这里,这里主要存放的是外部系统在调用当前系统服务时需要用到的常量。

简单举个例子,订单类型,在外部系统需要在当前系统中创建订单时,必须要指定对应的业务类型,下面简单贴下订单类型的枚举实现:

  import lombok.AllArgsConstructor;
  import lombok.Getter;
  import lombok.NoArgsConstructor;
  import org.apache.commons.lang3.StringUtils;

  import java.util.Arrays;

  @AllArgsConstructor
  @NoArgsConstructor
  @Getter
  public enum BizOrderBizTypeEnum {

        EMPLOAN("emp_loan"), 

        CASHLOAN("cash_loan"),

        VPAY("v_pay"), 

        VFQ("vfq"), 
;
private String orderBizType;

/**
 * 判断type是否合法
 *
 * @param type
 * @return
 */
public static boolean isIn(String type) {
    if (StringUtils.isBlank(type)) {
        return false;
    }

    return Arrays.asList(BizOrderBizTypeEnum.values()).parallelStream().anyMatch(value ->
            StringUtils.equalsIgnoreCase(value.getOrderBizType(), type));

   }
}

目前习惯用lombok注解来代替一些常用的getter/setter/constructor的代码实现,减少一些常用代码的生成。
PS: idea中安装好lombok插件后,需要在这里启用:


近期项目搭建上的一些实践及思考_第2张图片
Snip20180317_2.png

服务说明

包括服务接口、请求对象、响应对象以及对应的错误码,当然由于存在多个服务,这里根据读(查询)、写(创建、更新)将服务分成两个单独的服务说明类,尽量区分开不同的操作类型。

当然这里引入hibernate-validator框架的注解,对于一些传参做了检验,具体生效还需要在服务实现中处理。

简单贴下对应的服务说明实现:

   /*
    * @service interface class declared as service interface
    * version="1.0.0" service version
    */
@doc("订单创建服务")
@service(version="1.0.0")
interface BizOrderCreateService {

    @doc("创建订单")
    @ErrorSets(BizOrderErrorCode.class)
    BizOrderCreateResponse create(@tag(1)
                                  @required
                                  @NotNull
                                  @Valid BizOrderCreateRequest bizOrderCreateRequest);

    @doc("合同签定,对于有签约环节的,在sign服务直接创建,不需要调用此服务。而对于没有签约环节的,则调动此服务直接传入签约数据")
    @ErrorSets(BizOrderErrorCode.class)
    OrderBaseResponse createContract(@tag(1)
                                     @required
                                     @NotNull
                                     @Valid BizOrderContractCreateRequest contractCreateRequest);
}

其中对应的出入参(简要贴下,具体的属性略之):

@doc("订单服务的Base Request,公用数据")
class OrderBaseRequest {


    @doc("业务Code,一个订单具有唯一的Code信息,创建时可以随意传值但不可为空,主要依赖sourceId做幂等。其它场景需要传订单号")
    @required
    @NotBlank(message = "不能为空")
    String bizCode;


    @doc("操作类型,如新增订单、更新订单、新增订单规则、更新订单规则等")
    @required
    @NotBlank(message = "不能为空")
    String operationType;


    @required
    @doc("来源ID,每次请求均不相同,重试场景则要保重sourceId相同")
    @NotBlank(message = "不能为空")
    String sourceId;
}

@doc("订单服务Base Response")
class OrderBaseResponse {

    @doc("返回状态码 @see OrderErrorCode")
    @default_value("SUCCESS")
    String resultCode;


    @doc("返回结果说明")
    String msg;


    @doc("是否需要重试,默认为false")
    Boolean needRetry;
}
 /*
 * @struct this class declared as struct
 * 用于创建订单时的请求数据
 */
@doc("订单创建请求")
@struct
class BizOrderCreateRequest extends OrderBaseRequest {

    @required
    @doc("订单创建请求")
    @NotNull
    @Valid
    BizOrderCreateModel bizOrderCreateModel;

    @optional
    @doc("子订单列表")
    @Valid
    List subBizOrderCreateModels;

    @optional
    @doc("订单扩展信息")
    @Valid
    BizOrderExtendsCreateModel extendsCreateModel;

    @optional
    @doc("出资渠道信息,可有多个,发生资金交易后由资金系统回传过来")
    @Valid
    List channelModels;

    @optional
    @doc("活动信息,一个订单可能在多个活动叠加范围内")
    @Valid
    List activityModels;

    @optional
    @doc("用户借款的合同信息,如果是多方借款,可能会有多个合同,以及代扣合同")
    @Valid
    List contractModels;
}

@doc("订单创建结果")
@struct
class BizOrderCreateResponse extends OrderBaseResponse {

    @doc("生成的订单ID")
    @optional
    String bizOrderIdStr;

    @doc("出现子订单的情况下,返回对应的子订单列表")
    @optional
    List subBizOrderIdStrs;
}

说明:

  • @require @optional 是公司内部框架自定义注解,用于标注参数是否必传。其实可统一由hibernate-validator框架中的@NotBlank之类的注解来替代;@doc也是内部注解,用于说明当前标注的对象作用,类似于javadoc
  • 对于模型的复杂属性,比如对其他对象的引用,如果需要校验,需要加入hibernate-validator中的@Valid注解,表示会对其引用的对象中属性进行校验。

Service bundle结构

重头戏来了哈

先简单贴下目前的项目结构,然后再挨个说明:


近期项目搭建上的一些实践及思考_第3张图片
Snip20180317_3.png

首先大的src目录有两个,main及test,test中包含针对本bundle的单元测试代码,就不详细展开了哈,目前主要基于junit/testng编写。

main中目录结构:

先贴下图,然后挨个细化下具体分布及思路


近期项目搭建上的一些实践及思考_第4张图片
Snip20180317_5.png
java
  • convertor:用于存放接口出入参与底层数据模型的convertor
  • entity: 底层数据模型,对应DB中表结构及resources中的mapper文件
  • event:引入了spring event,目前主要用于异步记录订单的操作日志
  • exception:自定义了业务异常
  • manager:定义manager层接口,下面又区分了两层
    • impl: 查询接口的实现就直接落地在这里。而写服务对应的impl在这里只是一层封装,做部分不牵扯到外部依赖的数据校验,但是并不详细处理
    • bizImpl: 写服务的具体实现,每个服务对应一个bizImpl类,封装了统一的模板类,统一处理幂等、日志记录、异常处理、事务/回滚等通用实现;主要业务代码都集中在这里。
  • repository: 匹配mapper文件的mybatis接口
  • service.impl: 实现service-api中定义的接口,这里调用manager层的实现来做具体处理,另外记录统一出入日志、封装对外返回的结果数据。
  • util:常用工具类,比如对json、time、collection的处理
  • validator:配合service-api中对应的hibernate-validator注解来做实际的参数校验。
resources
  • mapper:存放mybatis对应的mapper.xml文件
  • properties: 存放不同环境(开发、测试、预发、线上)下的配置信息
  • sql:将目前的表结构DDL及部分基础数据sql存放在这里
  • applicationContext.xml: 对应的spring配置信息
  • logback.groovy:logback日志配置

这里简单对其中一部分目录进行记录:

validator:
近期项目搭建上的一些实践及思考_第5张图片
Snip20180317_6.png
  • ValidatorSupport:
    基于hibernate-validator做了校验的封装,实际实现代码如下:
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import javax.validation.ConstraintViolation;
import java.util.Set;
import java.util.stream.Collectors;


/**
  * 基于hibernate validator封装验证支持
  */
@Component
 public class ValidatorSupport {

private static final String VALIDATE_MSG_JOINER = ";";

@Autowired
private LocalValidatorFactoryBean validator;

/**
 * 执行验证
 *
 * @param object
 * @param 
 * @return
 */
public  Set> validate(final T object) {
    Set> constraintViolations = validator.validate(object);

    return constraintViolations;
}

/**
 * 生成特定格式验证失败信息
 *
 * @param constraintViolations
 * @param 
 * @return
 */
public static  String getValidateErrMsg(Set> constraintViolations) {
    if (CollectionUtils.isNotEmpty(constraintViolations)) {
        return constraintViolations.stream()
                .map(constraintViolation -> constraintViolation == null ?
                        StringUtils.EMPTY :
                        new StringBuilder(constraintViolation.getPropertyPath().toString())
                                .append(constraintViolation.getMessage()))
                .collect(Collectors.joining(VALIDATE_MSG_JOINER));
    }
    return StringUtils.EMPTY;
}

}

  • BaseValidateAction:
    校验父类,做了一些通用的校验实现
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.validation.ConstraintViolation;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.Set;

@Component
public class BaseValidateAction {

@Autowired
private ValidatorSupport validatorSupport;

/**
 * 组装异常响应
 *
 * @param joinPoint
 * @param be
 * @return
 */
protected Object getBusinessExceptionResponse(ProceedingJoinPoint joinPoint, BusinessException be) {
    try {
        Method method = getMethod(joinPoint);

        Class returnClass = method.getReturnType();

        Object mockResult = returnClass.newInstance();

        //原则上须保证当前方法出参均为OrderBaseResponse及其子类
        if (mockResult instanceof OrderBaseResponse) {
            ((OrderBaseResponse) mockResult).setResultCode(be.getErrorCode().toString());
            ((OrderBaseResponse) mockResult).setMsg(be.getMessage());
            return mockResult;
        }

        return mockResult;
    } catch (Exception e) {

    }

    return null;
}

private Method getMethod(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
    Signature sig = joinPoint.getSignature();

    MethodSignature signature;
    if (!(sig instanceof MethodSignature)) {
        throw new IllegalArgumentException("该注解只能用于方法");
    }
    signature = (MethodSignature) sig;

    Object target = joinPoint.getTarget();

    return target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
}

/**
 * 校验数据是否为空
 *
 * @param request
 * @throws BusinessException
 */
protected void checkRequestNonNull(Object request) throws BusinessException {
    if (Objects.isNull(request)) {
        throw new BusinessException(BizOrderErrorCode.ORDER_COMMON_ILLEGAL_ARGUMENT, "请求信息为空");
    }
}

/**
 * 校验注解数据
 *
 * @param request
 * @throws BusinessException
 */
protected void checkHibernateValidator(Object request) throws BusinessException {
    Set> requestConstraintViolations = validatorSupport.validate(request);
    if (!requestConstraintViolations.isEmpty()) {
        throw new BusinessException(BizOrderErrorCode.ORDER_COMMON_ILLEGAL_ARGUMENT,
                ValidatorSupport.getValidateErrMsg(requestConstraintViolations));
    }
}

}

  • 然后就是分别针对读服务及写服务的校验实现
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.assertj.core.util.Arrays;
import org.springframework.stereotype.Component;


/**
 * 查询服务请求校验
 */
@Aspect
@Component
@Order(1)
public class OrderQueryValidateAction extends BaseValidateAction {

@Autowired
private ValidatorSupport validatorSupport;


@Around("execution (* com.XXX.order.service.impl.*.find*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();

    //存在入参,执行入参校验
    if (!Arrays.isNullOrEmpty(args)) {
        try {
            for (Object request : args) {
                // 非空验证
                checkRequestNonNull(request);

                // Hibernate Validator注解验证
                checkHibernateValidator(request);
            }
        } catch (BusinessException e) {
            return getBusinessExceptionResponse(joinPoint, e);
        }
    }

    return joinPoint.proceed();
}
}

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.assertj.core.util.Arrays;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
  * 写服务请求校验
  */
@Aspect
@Component
@Order(1)
public class OrderValidateAction extends BaseValidateAction {

@Around("execution (* com.XXXX.order.service.impl.*.create(..)) " +
        "|| execution (* com.XXXX.order.service.impl.*.update*(..))" +
        "|| execution (* com.XXXX.order.service.impl.BizOrderStatusServiceImpl.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();

    //存在入参,执行入参校验
    if (!Arrays.isNullOrEmpty(args)) {
        try {
            for (Object request : args) {
                // 非空验证
                checkRequestNonNull(request);

                // Hibernate Validator注解验证
                checkHibernateValidator(request);

                // 枚举值验证
                checkEnum(request);

                // 自定义验证
                // checkCustom(request);
            }
        } catch (BusinessException e) {
            return getBusinessExceptionResponse(joinPoint, e);
        }
    }

    return joinPoint.proceed();
}

/**
 * 校验OperationTypeEnum
 *
 * @param request
 * @throws BusinessException
 */
private void checkEnum(Object request) throws BusinessException {
    String operationType = (String) ReflectionUtil.getValue(request, "operationType", "");

    if (Objects.nonNull(operationType) && !BizOrderOperationTypeEnum.isIn(operationType)) {
        throw new BusinessException(BizOrderErrorCode.ORDER_COMMON_ILLEGAL_ARGUMENT, "operationType不合法");
    }
}

/**
 * 自定义校验
 *
 * @param request
 * @throws BusinessException
 */
private void checkCustom(Object request) throws BusinessException {

    // TODO: 校验所有的attributes key是否来自Attributes中枚举

    // TODO 判断两层code是否一致

}

}

manager
近期项目搭建上的一些实践及思考_第6张图片
Snip20180317_7.png

这里解释下,为什么在service层又做了一个子的bizImpl层,每个bizImpl都对应一个manager中写服务实现方法,也就是强制从类级别隔离了每个方法的业务实现。同时针对写服务,做了对应的模板类,如下:

/**
  * 定义写服务的入口process模板方法
  *
  * @param 
  * @param 
  */
@FunctionalInterface
public interface BaseBizManager {

/**
 * process模板,用于处理通用写服务相关方法,包括处理幂等、记录日志、事务保证等
 *
 * @param request
 * @return
 * @throws BusinessException
 */
R process(T request, StateMachine... stateMachine) throws BusinessException;
}

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.statemachine.StateMachine;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.math.BigInteger;

/**
  * 主要处理订单 写DB操作,处理事务\幂等操作
  */
@Slf4j
public abstract class AbstractBizManagerImpl implements    BaseBizManager{

@Autowired
private BizOrderIdemRepository orderIdemRepository;

@Autowired
private BizOrderStateMachineContextPersistManager bizOrderStateMachineContextPersistManager;

@Autowired
private BizOrderLogEventPublisher bizOrderLogEventPublisher;

@Override
@Transactional(value = "finOrderocTransactionManager", rollbackFor = {BusinessException.class})
public R process(T request, StateMachine... stateMachines) throws BusinessException {
    try {
        // 幂等控制
        if (checkIdem(request)) {
            log.info("check idempotence bingo,request={}", request);
            throw new BusinessException(BizOrderErrorCode.SUCCESS, "幂等操作,本次请求忽略");
        }

        // 实际业务处理
        R resp = doProcess(request, stateMachines);
        log.info("response = {}", resp);

        return resp;
    } catch (BusinessException e) {
        log.error("process Business Exception = {}", e);
        throw new BusinessException(e.getErrorCode(), ExceptionUtil.getErrorMsg(e));
    } catch (Exception e) {
        log.error("process Exception = {}", e);
        throw new BusinessException(BizOrderErrorCode.ORDER_GENERIC_EXCEPTION, ExceptionUtil.getErrorMsg(e));
    }
}

/**
 * 实际的业务操作
 *
 * @param request 业务请求
 * @param stateMachine 将上游处理后的stateMachine传递进来,后续持久化
 * @return 业务结果
 */
abstract R doProcess(T request, StateMachine... stateMachine) throws Exception;

/**
 * 判断是否幂等
 * 幂等 ==> 返回true
 *
 * @param request
 * @return
 */
private boolean checkIdem(T request) {
    boolean result = false;
    // 反射获取请求中基础数据
    String orderCode = (String) ReflectionUtil.getValue(request, "bizCode", "");
    String operationType = (String) ReflectionUtil.getValue(request, "operationType", "");
    String sourceId = (String) ReflectionUtil.getValue(request, "sourceId", "");

    String idemNo = orderCode + operationType + sourceId;
    BizOrderIdem idem = new BizOrderIdem(idemNo, new BigInteger(orderCode));

    // 违反唯一性约束
    try {
        orderIdemRepository.insert(idem);
    } catch (DuplicateKeyException e) {
        result = true;
        log.error("接口重复消费, idemNo = {}, orderCode = {}, exception = {}", idemNo, orderCode, e);
    } catch (Exception e) {
        log.error("未知异常,exception={}", e);
    }

    return result;
}
 }

这样每个bizImpl仅需要实现对应的 doProcess 方法即可。

你可能感兴趣的:(近期项目搭建上的一些实践及思考)