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

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

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

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

Service-Api bundle

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

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插件后,需要在这里启用:


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<BizOrderCreateModel> subBizOrderCreateModels;

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

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

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

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

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

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

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

说明:

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

Service bundle结构

重头戏来了哈

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


Snip20180317_3.png

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

main中目录结构:

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


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:
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 <T>
 * @return
 */
public <T> Set<ConstraintViolation<T>> validate(final T object) {
    Set<ConstraintViolation<T>> constraintViolations = validator.validate(object);

    return constraintViolations;
}

/**
 * 生成特定格式验证失败信息
 *
 * @param constraintViolations
 * @param <T>
 * @return
 */
public static <T> String getValidateErrMsg(Set<ConstraintViolation<T>> 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<ConstraintViolation<Object>> 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
Snip20180317_7.png

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

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

/**
 * process模板,用于处理通用写服务相关方法,包括处理幂等、记录日志、事务保证等
 *
 * @param request
 * @return
 * @throws BusinessException
 */
R process(T request, StateMachine<BizOrderStatusEnum, BizOrderStatusChangeEventEnum>... 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<T,R> implements    BaseBizManager<T,R>{

@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<BizOrderStatusEnum, BizOrderStatusChangeEventEnum>... 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<BizOrderStatusEnum, BizOrderStatusChangeEventEnum>... 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 方法即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容