最近在参与公司内部的中台建设,同时启动了多个项目,因此形成一套统一的项目搭建思路及架构就变得比较迫切,这里将最近的一些成果,包括趟过的坑记录下来,汇总下。当然之后如果个人能力有所成长,肯定还有更多改动,拥抱变化嘛!
由于公司内部有一套还算成熟的rpc框架,也强制要求必须使用,就没有采用外部比较流行的dubbo、spring boot等开源框架,不过今天的重点不在这里,而在于项目内部的结构设计及一些公用实现,下面开始展开。
一个项目,如果需要对外提供服务,目前最简单的通用做法是创建两个bundle,一个负责具体的业务实现service-bundle,另外一个负责对外提供client-jar包,首先说下client bundle(我这边取名为service-api)
Service-Api bundle
先贴下我这边项目的对应结构:(忽略build及out目录,另外屏蔽掉了部分敏感包名信息哈)
一个完备的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插件后,需要在这里启用:
服务说明
包括服务接口、请求对象、响应对象以及对应的错误码,当然由于存在多个服务,这里根据读(查询)、写(创建、更新)将服务分成两个单独的服务说明类,尽量区分开不同的操作类型。
当然这里引入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结构
重头戏来了哈
先简单贴下目前的项目结构,然后再挨个说明:
首先大的src目录有两个,main及test,test中包含针对本bundle的单元测试代码,就不详细展开了哈,目前主要基于junit/testng编写。
main中目录结构:
先贴下图,然后挨个细化下具体分布及思路
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:
- 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
这里解释下,为什么在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 方法即可。