# xService **Repository Path**: javacoo/xService ## Basic Information - **Project Name**: xService - **Description**: XService接口服务快速开发框架,基于SpringBoot实现,封装了接口开发过程中的基础功能及控制流程,并约定了统一的接口报文格式,制定了完善的开发规范以及测试规范,让程序员只需关注具体业务实现,提高了开发接口服务的效率。 XService基础功能基于xkernel 提供的SPI机制,结合SpringBoot提供的 ConditionalOnBean,ConditionalOnProperty等注解实现,实用,简单,扩展灵活。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 3 - **Created**: 2020-05-28 - **Last Updated**: 2024-10-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # XService:为组件化,快速构建微服务而生 ## 什么是XService? XService接口服务快速开发框架,基于SpringBoot实现,封装了接口开发过程中的基础功能及控制流程,并约定了统一的接口报文格式,制定了完善的开发规范以及测试规范,让程序员只需关注具体业务实现,提高了开发接口服务的效率。 XService基础功能基于xkernel 提供的SPI机制,结合SpringBoot提供的 ConditionalOnBean,ConditionalOnProperty等注解实现,实用,简单,扩展灵活。 ## 安装 & 入门 如果你是使用`Maven`来构建项目,你需要添加XService的`pom.xml`文件内,如下所示: ```xml com.javacoo.xservice xservice_base 1.0.0 ``` 添加完组件我们就可以进行配置使用了。 ## 使用指南 ##### XService的约定 - 请求协议公共部分 | 参数 | 类型 | 是否必选 | 描述 | | ------------- | ------ | -------- | ------------------------------------------------------------ | | appKey | String | 是 | 应用key | | nonce | String | 是 | 32位UUID随机字串,格式如:296f6fdd570244d98b6046ec135a5b8a | | sign | String | 是 | 签名 | | timestamp | Long | 是 | 请求时间戳,格式:1483668094200绝大部分接口中都需要传入时间戳值进行验证,时间戳值为从格林威治时间1970年1月1日0时0分0秒起至现在的总豪秒数,用于精确指明请求发起时间。例如2014年1月1日0点0分0秒这一刻的时间戳值为1391184000195。为了防止重放攻击,服务器接收时若判读出当前时间已超过服务器配置有效时间范围,则视为无效请求。 | | transactionSn | String | 是 | 交易流水号 | | parameter | Object | 否 | 请求的业务对象 | - 响应协议公共部分 | 参数 | 类型 | 描述 | | ------------- | ------ | ------------------------ | | code | String | 返回码,具体含有参见下文 | | message | String | 返回消息,如错误信息 | | timestamp | String | 响应时间 | | transactionSn | String | 交易流水号 | | sign | String | 签名 | | data | Object | 返回的业务对象 | - 返回码 平台级返回码如下:业务可制定具体的业务返回代码 | code | message | 说明 | | ---- | ------------------- | ------------------------------------------------------------ | | 200 | 请求成功 | | | 207 | 频繁操作 | 频繁操作 | | 400 | 请求参数出错 | 终端传递的参数值错误 | | 403 | 没有权限 | 没有权限 | | 404 | 服务不存在 | 服务不存在 | | 408 | 请求超时 | 请求超时 | | 409 | 业务逻辑出错 | 服务端执行服务方法时,执行业务逻辑校验出错,或者响应数据为空。 | | 500 | 系统繁忙,请稍后再试 | 数据不满足提交条件或服务端执行服务方法时出现异常,需由服务人员解决 | - 签名/验签 签名算法:HEX(SHA256(secretKey+参数字符串+随机数+时间戳+secretKey)) 说明: 1:将需要签名的参数按参数名升序排序 2:请求参数串=paramName1paramValue1paramName2paramValue2... 3:将应用密钥分别添加到 请求参数串+随机数+时间戳 的头部和尾部:secretKey+请求参数字符串+随机数+时间戳+secretKey 4:对该字符串进行 SHA256 运算,得到一个byte数组 5:将该byte数组转换为十六进制的字符串,该字符串即是这些请求参数对应的签名 6:HEX(SHA256(secretKey+请求参数字符串+随机数+时间戳+secretKey)) **注意:仅针对基本数据类型及其包装类字段进行签名** #### 参考实现: ```java 依赖 fastjson,CHARSET = "UTF-8" /** * 签名 *

说明:

*
  • * @author DuanYong * @param paramMap 参数Map(使用fastjson工具包将请求业务参数对象转Map) * @param nonce 随机数 * @param timestamp 时间戳 * @param secretKey 安全KEY * @return 签名值 */ public final static String clientSign(Map paramMap,String nonce, String timestamp, String secretKey) { if (param == null || param.length() == 0 || secretKey == null || secretKey.length() == 0){ return null; } try { byte[] sei = getSecretKey(secretKey); return sign(paramMap,nonce,timestamp,sei); } catch (Exception e) { return null; } } /** * 验证签名 *

    说明:

    *
  • * @author DuanYong * @param signedMsg 返回的签名字符串 * @param returnMap 返回的对象(使用fastjson工具包将响应的业务对象转Map) * @param timestamp 时间戳 * @param nonce 随机数 * @param secretkey 安全KEY */ public static boolean verifySign(String signedMsg,Map returnMap,String nonce,String timestamp,String secretkey){ try{ byte[] sei = getSecretKey(secretkey); String sign = sign(returnMap,nonce,timestamp,sei); if(null == sign || null == signedMsg || !sign.equals(signedMsg)){ return false; } return true; }catch(Exception e){ e.printStackTrace(); } return false; } /** * 签名 *

    说明:

    *
  • 1:将需要签名的参数按参数名升序排序
  • *
  • 2:请求参数串=paramName1paramValue1paramName2paramValue2...
  • *
  • 3:将应用密钥分别添加到 请求参数串+随机数+时间戳 的头部和尾部:secret+请求参数字符串+随机数+时间戳+secret
  • *
  • 4:对该字符串进行 SHA256 运算,得到一个byte数组
  • *
  • 5:将该byte数组转换为十六进制的字符串,该字符串即是这些请求参数对应的签名
  • *
  • 6:HEX(SHA256(secret+请求参数字符串+随机数+时间戳+secret))
  • * @author DuanYong * @param params 需要签名的参数 * @param secretKey 应用密钥 * @return 签名值 */ private final static String sign(Map params,String nonce, String timestamp, byte[] secretKey) { if (params == null || params.size() == 0 || secretKey == null){ return null; } try { List paramNames = new ArrayList<>(params.size()); paramNames.addAll(params.keySet()); Collections.sort(paramNames); StringBuilder sb = new StringBuilder(); for (String paramName : paramNames) { Object v = params.get(paramName); if(v instanceof JSONArray || v instanceof JSONObject){ continue; } sb.append(paramName).append(v); } sb.append(nonce); sb.append(timestamp); List bytes = new ArrayList<>(); bytes.add(secretKey); bytes.add(sb.toString().getBytes(CHARSET)); bytes.add(secretKey); byte[] data = addBytes(bytes); return byte2hex(encodeSHA256(data)); } catch (Exception e) { return null; } } private static byte[] getSecretKey(String secretkey) throws Exception{ return secretkey.getBytes(CHARSET); } private static byte[] addBytes(List bytes){ int len = 0; for(byte[] b : bytes){ len = len + b.length; } byte[] newData = new byte[len]; int tempLen = 0; for(byte[] b : bytes){ System.arraycopy(b,0,newData,tempLen,b.length); tempLen = tempLen+b.length; } return newData; } private static String byte2hex(byte[] bytes) { StringBuilder sign = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() == 1) { sign.append("0"); } sign.append(hex); } return sign.toString(); } private static byte[] encodeSHA256(byte[] data) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); return md.digest(data); } ``` - 加密/解密 加密算法:Base64(DES(value,secretKey)) 解密算法:DES(Base64(value),secretKey) #### 参考实现: ```java public static final String decryptDes(String cryptData, String key) { String decryptedData = null; try { // 把字符串解码为字节数组,并解密 decryptedData = new String(decrypt(decryptBASE64(cryptData), key.getBytes())); } catch (Exception e) { throw new RuntimeException("解密错误,错误信息:", e); } return decryptedData; } public static final String encryptDes(String data, String key) { String encryptedData = null; try { // 加密,并把字节数组编码成字符串 encryptedData = encryptBASE64(encrypt(data.getBytes(), key.getBytes("UTF-8"))); } catch (Exception e) { throw new RuntimeException("加密错误,错误信息:", e); } return encryptedData; } private static byte[] decrypt(byte[] data, byte[] key) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { // 还原密钥 Key k = toKey(key); // 实例化 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); // 初始化,设置为解密模式 cipher.init(Cipher.DECRYPT_MODE, k); // 执行操作 return cipher.doFinal(data); } public static byte[] encrypt(byte[] data, byte[] key) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidKeySpecException { // 还原密钥 Key k = toKey(key); // 实例化 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); // 初始化,设置为加密模式 cipher.init(Cipher.ENCRYPT_MODE, k); // 执行操作 return cipher.doFinal(data); } private static final byte[] decryptBASE64(String key) { try { return decode(key); } catch (Exception e) { throw new RuntimeException("解密错误,错误信息:", e); } } private byte[] decode(String str) throws IOException { byte[] arrayOfByte = str.getBytes(); ByteArrayInputStream inputStream = new ByteArrayInputStream(arrayOfByte); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); decodeBuffer(inputStream, outputStream); return outputStream.toByteArray(); } ``` - 定义接口服务 XService约定一个接口服务为一个Controller类,且此类必须继承框架提供的三个基类之一。 带参数的接口服务基类:AbstractParamController ```java /** * 业务参数控制器基类 *

    说明:

    *
  • 定义有业务参数的接口处理基本流程
  • * @author DuanYong * @param

    参数 * @since 2017年6月28日下午2:48:27 */ @Slf4j public abstract class AbstractParamController

    extends BaseController { /** * 接口处理 *

    说明:

    *
  • 1:请求参数解析
  • *
  • 2:检查请求参数
  • *
  • 3:业务处理
  • *
  • 4:设置响应数据
  • * @author DuanYong * @since 2017年6月28日下午3:19:43 * @param response 响应对象 */ @RequestMapping public final void handle(HttpServletResponse response) { final Long startTime = System.currentTimeMillis(); //参数解析->检查请求参数->业务处理->设置响应数据 parse().map(r->validateFunction.apply(r)).map(r->Optional.ofNullable(execute(r))).map(o->setSuccessResponse(response,o.orElse(Optional.empty()))); log.info("接口->{},处理完成,耗时->{}秒,流水号:{}", SwapAreaUtils.getSwapAreaData().getReqMethod(),(System.currentTimeMillis() - startTime)/1000.0,SwapAreaUtils.getSwapAreaData().getTransactionSn()); } /** * 执行 *

    说明:

    *
  • hystrix
  • * @author DuanYong * @since 2017年11月13日下午3:41:04 * @param p 业务参数 * @return: java.lang.Object 业务返回对象 */ private final Object execute(P p){ return executeFunction.apply(p); } /** * 解析请求参数 *

    说明:

    *
  • 将请求参数中的业务参数对象转换为服务使用的对象
  • * @author DuanYong * @since 2017年6月28日下午3:17:32 * @return: java.util.Optional

    业务参数对象 */ protected final Optional

    parse(){ BaseRequest baseRequest = SwapAreaUtils.getSwapAreaData().getBaseRequest(); baseRequest.getParameter().orElseThrow(()->new IllegalParameterException(Resources.getMessage(ErrorCodeConstants.COMMON_REQ_PARAM_PARSE_IS_EMPTY))); try{ return baseRequest.getParameter().map(o->o.toString()).map(s->initBaseParameter(s,baseRequest)); }catch(Exception ex){ ex.printStackTrace(); log.error("将请求参数中的业务参数对象转换为服务使用的对象失败,流水号:{},请求参数:{},异常信息:", WebUtil.getSwapAreaData().getTransactionSn(),baseRequest.getParameter(),ex); throw new IllegalParameterException(Resources.getMessage(ErrorCodeConstants.COMMON_REQ_PARAM_PARSE_ERROR)); } } /** * 初始化初始请求参数 *

    说明:

    *
  • 解析并初始化请求参数对象
  • * @author DuanYong * @param paramString 参数原始json字符串 * @param baseRequest 请求参数对象 * @return P 业务参数对象 * @since 2017年11月14日上午11:07:19 */ private P initBaseParameter(String paramString, BaseRequest baseRequest){ P p = FastJsonUtil.toBean(paramString,getParamClass()); p.setTransactionSn(baseRequest.getTransactionSn()); p.setQueryStringMap(baseRequest.getQueryStringMap()); return p; } /** * 校验请求中的业务参数 *

    说明:

    *
  • 由子类实现,如果参数检查不通过,请抛出参数异常:IllegalParameterException
  • * @author DuanYong * @param p 业务参数对象 * @throws IllegalParameterException * @since 2017年6月28日下午2:28:10 */ protected abstract void validate(P p) throws IllegalParameterException; /** * 具体业务处理 *

    说明:

    *
  • 由子类实现
  • * @author DuanYong * @param p 业务参数对象 * @return 业务返回数据 * @since 2017年5月5日下午3:24:09 */ protected abstract Object process(P p); /** * 获取参数类型 *

    说明:

    *
  • * @author DuanYong * @return 参数类型对象 * @since 2017年7月24日上午10:33:30 */ protected abstract Class

    getParamClass(); /** * 服务降级,默认返回REQUEST_TIMEOUT字符串,框架统一处理抛出TimeoutException异常 *

    说明:

    *
  • 注意:在fallback方法中不允许有远程方法调用,方法尽量要轻,调用其他外部接口也要进行hystrix降级。否则执行fallback方法会抛出异常
  • * @author DuanYong * @param p 参数 * @return REQUEST_TIMEOUT * @since 2018年8月21日上午11:20:37 */ protected Object fallback(P p){ return Constants.REQUEST_TIMEOUT; } /** * 校验并返回业务参数 */ private Function validateFunction = (P p)->{ validate(p); return p; }; /** * 执行业务处理 */ private Function executeFunction = (P p)-> process(p); /** * 执行降级业务处理 */ private Function fallbackFunction = (P p)-> fallback(p); } ``` 无参数的接口服务基类:AbstractNonParamController ```java /** * 无业务参数控制器基类 *

    说明:

    *
  • 定义无业务参数接口处理基本流程
  • *
  • 统一异常处理
  • * @author DuanYong * @since 2017年7月11日上午8:49:58 */ @Slf4j public abstract class AbstractNonParamController extends BaseController { /** * 具体业务处理 *

    说明:

    *
  • 由子类实现
  • * @author DuanYong * @return 业务返回数据 * @since 2017年7月11日上午8:51:23 */ protected abstract Object process(); /** * 接口处理 *

    说明:

    *
  • 业务处理
  • *
  • 设置响应数据
  • * @since 2017年7月11日上午9:13:28 */ @RequestMapping private final void handle(HttpServletResponse httpServletResponse) { Long startTime = System.currentTimeMillis(); //业务处理->设置响应数据 Optional.ofNullable(execute()).map(o->setSuccessResponse(httpServletResponse,o)); log.info("接口->{},处理完成,耗时->{}秒,流水号:{}", SwapAreaUtils.getSwapAreaData().getReqMethod(),(System.currentTimeMillis() - startTime)/1000.0,SwapAreaUtils.getSwapAreaData().getTransactionSn()); } /** * 执行 *

    说明:

    * @author DuanYong * @return: java.lang.Object 业务返回数据 * @since 2017年11月13日下午3:41:04 */ private final Object execute(){ return executeFunction.get(); } /** * 执行业务处理 */ private Supplier executeFunction = ()-> process(); } ``` url参数接口服务基类:AbstractUrlParamController ```java /** * 业务参数控制器基类 *

    说明:

    *
  • 定义有业务参数的接口处理基本流程
  • * @author DuanYong * @since 2017年6月28日下午2:48:27 */ @Slf4j public abstract class AbstractUrlParamController extends BaseController { /** * 接口处理 *

    说明:

    *
  • 1:请求参数解析
  • *
  • 2:检查请求参数
  • *
  • 3:业务处理
  • *
  • 4:设置响应数据
  • * @author DuanYong * @since 2017年6月28日下午3:19:43 */ @RequestMapping public final void handle(HttpServletResponse response) { Long startTime = System.currentTimeMillis(); //参数解析->检查请求参数->业务处理->设置响应数据 parse().map(r->validateFunction.apply(r)).map(r->Optional.ofNullable(execute(r))).map(o->setSuccessResponse(response,o.orElse(Optional.empty()))); log.info("接口->{},处理完成,耗时->{}秒,流水号:{}", SwapAreaUtils.getSwapAreaData().getReqMethod(),(System.currentTimeMillis() - startTime)/1000.0,SwapAreaUtils.getSwapAreaData().getTransactionSn()); } /** * 执行 *

    说明:

    * @author DuanYong * @param p 请求参数 * @return Object 业务返回数据 * @since 2017年11月13日下午3:41:04 */ private final Object execute(Map p){ return executeFunction.apply(p); } /** * 解析请求参数 *

    说明:

    *
  • 将URL请求参数中的业务参数对象转换为服务使用的MAP对象
  • * @author DuanYong * @since 2017年6月28日下午3:17:32 * @return: java.util.Optional> 业务参数对象 */ protected final Optional> parse(){ BaseRequest baseRequest = SwapAreaUtils.getSwapAreaData().getBaseRequest(); if(baseRequest.getQueryStringMap().isEmpty()){ log.error("解析URL请求参数失败,请求参数为空,流水号:{}", WebUtil.getSwapAreaData().getTransactionSn()); throw new IllegalParameterException(Resources.getMessage(ErrorCodeConstants.COMMON_REQ_PARAM_PARSE_IS_EMPTY)); } return Optional.ofNullable(baseRequest.getQueryStringMap()); } /** * 校验请求中的业务参数 *

    说明:

    *
  • 由子类实现,如果参数检查不通过,请抛出参数异常:IllegalParameterException
  • * @author DuanYong * @param p 业务参数对象 * @throws IllegalParameterException * @since 2017年6月28日下午2:28:10 */ protected abstract void validate(Map p) throws IllegalParameterException; /** * 具体业务处理 *

    说明:

    *
  • 由子类实现
  • * @author DuanYong * @param p 业务参数对象 * @return 业务返回数据 * @since 2017年5月5日下午3:24:09 */ protected abstract Object process(Map p); /** * 校验并返回业务参数 */ private Function,Map> validateFunction = (Map p)->{ validate(p); return p; }; /** * 执行业务处理 */ private Function,Object> executeFunction = (Map p)-> process(p); } ``` - 其他约定: 服务开发过程中尽量少使用多线程,如果使用了多线程,框架提供的交换区对象(ThreadLocal实现)将无法正常使用。 打印日志请使用LogUtil中的方法,因为框架对日志输出进行了增强,统一添加了流水号(基于交换区对象)。 LogUtil只能在当前主线程下使用。 ##### 使用示例 - 定义业务接口协议:协议公共部分+协议业务部分 请求协议业务部分如下:parameter 对象 | 参数 | 类型 | 是否必选 | 描述 | | ---- | ------ | -------- | -------- | | id | String | 是 | 业务主键 | 响应协议业务部分如下:data 对象 | 参数 | 类型 | 描述 | | ---- | ------- | ---- | | id | Integer | 主键 | | data | String | 数据 | - 编写实现代码 获取案例数据接口,带参数Controller:ExampleController ```java /** * 获取案例数据接口,带参数 *

    说明:

    *
  • * @author DuanYong * @since 2017年7月17日上午9:02:56 */ @Slf4j @RestController @RequestMapping(value = "/example/v1/getExampleInfo") public class ExampleController extends AbstractParamController { /** 数据服务 */ @Autowired private ExampleService exampleService; @Override protected void validate(BaseReq p) throws IllegalParameterException { AbstractAssert.notNull(p, ErrorCodeConstants.SERVICE_REQ_PARAM); AbstractAssert.isNotBlank(p.getId(), ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID); } @Override public Object process(BaseReq p) { log.info("执行业务方法"); return exampleService.getExampleInfo(p.getId()).get(); } @Override protected Class getParamClass() { return BaseReq.class; } } ``` 获取案例数据接口,无参数Controller:ExampleNonParamController ```java /** * 获取案例数据接口,无参数 *

    说明:

    *
  • * @author DuanYong * @since 2017年7月17日上午9:02:56 */ @RestController @RequestMapping(value = "/example/v1/getNonParamExampleInfo") public class ExampleNonParamController extends AbstractNonParamController { /** 数据服务 */ @Autowired private ExampleService exampleService; @Override public Object process() { return exampleService.getExampleInfo("1"); } } ``` 获取案例数据接口,url参数Controller:ExampleUrlParamController ```java /** * 获取案例数据接口,url参数 *

    说明:

    *
  • * @author DuanYong * @since 2017年7月17日上午9:02:56 */ @Slf4j @RestController @RequestMapping(value = "/example/v1/getUrlParamExampleInfo") public class ExampleUrlParamController extends AbstractUrlParamController { /** 数据服务 */ @Autowired private ExampleService exampleService; @Override protected void validate(Map p) throws IllegalParameterException { log.info("validate->{}",p); } @Override protected Object process(Map p) { return exampleService.getExampleInfo(p.get("id")); } } ``` 请求业务对象:BaseReq ```java /** * 查询对象基类 *

    说明:

    *
  • 定义相关公共查询字段
  • * @author DuanYong * @since 2017年7月17日上午8:55:10 */ @Data @AllArgsConstructor @NoArgsConstructor public class BaseReq extends BaseParameter { /** * ID */ private String id; } ``` 响应业务对象:ExampleDto ```java /** * 参数 *

    说明:

    *
  • * @author DuanYong * @since 2017年7月14日下午1:04:59 */ @Data @AllArgsConstructor @NoArgsConstructor public class ExampleDto { /** * id */ private String id; /** * 数据 */ private String data; } ``` 编写:ExampleDao及ExampleDaoMapper.xml ```java /** * Example服务DAO *

    说明:

    *
  • * @author DuanYong * @since 2017年7月14日下午1:37:04 */ public interface ExampleDao { /** * 根据版块ID ,查询版块内容 *

    说明:

    *
  • * @author DuanYong * @param id * @return ExampleDto * @since 2017年7月14日下午1:40:26 */ ExampleDto getExampleInfo(@Param("id")String id); } ``` ExampleDaoMapper.xml ```xml ``` 定义服务接口:ExampleService ```java /** * 案例数据服务接口 *

    说明:

    *
  • 获取详细数据
  • * @author DuanYong * @since 2017年7月14日上午10:54:21 */ public interface ExampleService { /** * 获取版块及版块下内容信息 *

    说明:

    *
  • * @author DuanYong * @param id 参数 * @return * @since 2017年7月14日上午11:23:21 */ Optional getExampleInfo(String id); } ``` 实现服务:ExampleServiceImpl ```java /** * 案例数据服务接口实现 *

    说明:

    *
  • * @author DuanYong * @since 2017年7月14日下午1:30:18 */ @Service @Slf4j public class ExampleServiceImpl implements ExampleService { @Autowired private ExampleDao exampleDao; @Override public Optional getExampleInfo(String id) { AbstractAssert.isNotBlank(id, ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID); return Optional.ofNullable(exampleDao.getExampleInfo(id)); } } ``` - 编写测试 测试基类 ```java /** * 测试基类 *
  • * @author duanyong@jccfc.com * @date 2020/10/16 15:58 */ @Slf4j @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) public class BaseTest { // 安全密钥 protected static final String SECRET_KEY = "5c06aedadb259698dc59f64fc02f4488d32fb2fd298d156873e23ed37311a2b600000018"; // 渠道 protected static final String APP_KEY = "CHENNEL_1"; protected static String getNonce() { return WebUtil.genTransSn(); } /** * 校验结果 *
  • * @author duanyong@jccfc.com * @date 2021/3/3 14:59 * @param result: 结果 * @return: void */ protected void verify(String result){ if(StringUtils.isBlank(result)){ return; } BaseResponse baseResponse = FastJsonUtil.toBean(result,BaseResponse.class); if(StringUtils.isBlank(baseResponse.getSign())){ return; } baseResponse.getData().ifPresent(o->{ String s = FastJsonUtil.toJSONString(o); log.info("请求返回业务json:{}",s); log.info("请求返回签名:{}",baseResponse.getSign()); if (SignUtil.cloudVerifySign(baseResponse.getSign(), s,baseResponse.getTransactionSn(),baseResponse.getTimestamp().toString(), SECRET_KEY)) { log.info("返回数据合法"); } else { log.info("返回数据被篡改"); } }); } } ``` 测试类:ExampleControllerTest ```java /** * 接口测试 *
  • * * @author: duanyong@jccfc.com * @since: 2021/3/3 13:49 */ @Slf4j public class ExampleControllerTest extends BaseTest { private MockMvc mockMvc; @Autowired private WebApplicationContext wac; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); } @Test public void getExampleInfoTest() throws Exception{ MvcResult mvcResult = mockMvc.perform(post("/example/v1/getExampleInfo") .contentType(MediaType.APPLICATION_JSON) .content(FastJsonUtil.toJSONString(getExampleInfoReq()))) .andExpect(status().isOk())// 模拟发送post请求 .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))// 预期返回值的媒体类型text/plain;charset=UTF-8 .andReturn();// 返回执行请求的结果 String result = mvcResult.getResponse().getContentAsString(); log.info("请求的结果:{}",result); verify(result); } private Object getExampleInfoReq() { BaseRequest baseRequest = new BaseRequest(); baseRequest.setAppKey(APP_KEY); baseRequest.setTimestamp(Calendar.getInstance().getTimeInMillis()); baseRequest.setNonce(getNonce()); baseRequest.setTransactionSn(getNonce()); BaseReq baseReq = new BaseReq(); baseReq.setId(SecurityUtil.encryptDes("1",SECRET_KEY)); String json = FastJsonUtil.toJSONString(baseReq); String sign = SignUtil.clientSign(json,baseRequest.getNonce(),baseRequest.getTimestamp().toString(),SECRET_KEY); baseRequest.setSign(sign); baseRequest.setParameter(baseReq); return baseRequest; } } ``` - 测试日志 ```properties [ main] c.j.x.b.interceptor.HandlerInterceptor : 接口->getExampleInfo,原始POST请求参数:{"appKey":"CHENNEL_1","nonce":"20210309094818488EEE50B9E9061026","parameter":{"id":"lbGSlXJ0ZB7="},"parameterMap":{"id":"lbGSlXJ0ZB7="},"sign":"0defb4240c72d52e5b46803ce51a728f8650ccafa9b80f9dbd4e7cd716c0fa1d","timestamp":1615254498483,"transactionSn":"20210309094818490626096EFA956655"},原始URL请求参数:null,流水号:202103090948185408BA9FAB01255050 [ main] c.j.x.b.s.handler.MethodLockHandler : 方法进入分布式事务锁,加锁key:getExampleInfo-lbGSlXJ0ZB7=,自动失效时间:10秒 [ main] c.j.x.b.cache.redis.lock.RedssionLock : >>>>> tryLock lockKey:javacoo:service:lock:getExampleInfo-lbGSlXJ0ZB7=,TimeUnit:SECONDS,waitTime:0,timeout:10 [ main] c.j.x.b.s.handler.MethodLockHandler : 加锁成功,KEY:getExampleInfo-lbGSlXJ0ZB7=,自动失效时间:10秒 [ main] c.j.x.b.s.handler.ParamValidatorHandler : 接口上送的签名值:0defb4240c72d52e5b46803ce51a728f8650ccafa9b80f9dbd4e7cd716c0fa1d [ main] c.j.x.b.support.handler.EnDeCodeHandler : 接口:getExampleInfo,参数对象:{id=lbGSlXJ0ZB7=},加解密字段:['id'] [ main] c.j.x.b.interceptor.HandlerInterceptor : 接口->getExampleInfo,预处理完成,耗时->0.489秒,流水号:20210309094818490626096EFA956655 [ main] c.j.x.e.controller.ExampleController : 执行业务方法 [ main] org.mybatis.spring.SqlSessionUtils : Creating a new SqlSession [ main] org.mybatis.spring.SqlSessionUtils : SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@b87ff6] was not registered for synchronization because synchronization is not active [ main] com.zaxxer.hikari.HikariDataSource : MyHikariCP - Starting... [ main] com.zaxxer.hikari.HikariDataSource : MyHikariCP - Start completed. [ main] o.m.s.t.SpringManagedTransaction : JDBC Connection [HikariProxyConnection@30621512 wrapping com.mysql.cj.jdbc.ConnectionImpl@1a226ea] will not be managed by Spring [ main] org.mybatis.spring.SqlSessionUtils : Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@b87ff6] [ main] c.j.x.base.AbstractParamController : 接口->getExampleInfo,处理完成,耗时->1.366秒,流水号:20210309094818490626096EFA956655 [ main] c.j.x.b.support.handler.EnDeCodeHandler : 接口:getExampleInfo,参数对象:{data=data, id=1},加解密字段:['id'],['data'] [ main] c.j.x.b.cache.redis.lock.RedssionLock : >>>>> unlock lockKey:javacoo:service:lock:getExampleInfo-lbGSlXJ0ZB7= [ main] c.j.x.b.s.handler.MethodUnLockHandler : 方法解锁,MethodName:getExampleInfo,key:getExampleInfo-lbGSlXJ0ZB7=,流水号:20210309094818490626096EFA956655 [ main] c.j.x.b.interceptor.HandlerInterceptor : 接口->getExampleInfo,后置处理完成,耗时->0.014秒,流水号:20210309094818490626096EFA956655 [pool-2-thread-4] c.j.x.b.s.e.h.TransCompleteEventHandler : 交易完成事件处理:com.javacoo.xservice.base.support.event.TransCompleteEvent[source=com.javacoo.xservice.base.interceptor.HandlerInterceptor@1266391] [ main] c.j.x.e.c.ExampleControllerTest : 请求的结果:{"code":"200","data":{"data":"BOmRuJy1ta7=","id":"lbGSlXJ0ZB7="},"message":"请求成功","sign":"e8b4b758265a8a00d7628565d8fa17d7baeec7ebc4b3f01882b6ac56816e7d88","timestamp":1615254500416,"transactionSn":"20210309094818490626096EFA956655"} [ main] com.javacoo.xservice.example.BaseTest : 请求返回业务json:{"data":"BOmRuJy1ta7=","id":"lbGSlXJ0ZB7="} [ main] com.javacoo.xservice.example.BaseTest : 请求返回签名:e8b4b758265a8a00d7628565d8fa17d7baeec7ebc4b3f01882b6ac56816e7d88 [ main] com.javacoo.xservice.example.BaseTest : 返回数据合法 ``` - 配置及说明 ```properties profile = dev_envrimont spring.http.encoding.force=true #监控 #management.security.enabled=false #management.port=54007 spring.datasource.url=jdbc:mysql://mysql01.io:3306/dev?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=25&prepStmtCacheSqlLimit=256&characterEncoding=UTF-8&allowMultiQueries=true&autoReconnect=true&roundRobinLoadBalance=true spring.datasource.username=root #SecurityUtil解密,以DES@+密文 spring.datasource.password=DES@JXAR60ozSXSBMvQLcOMhmKyhh0Ua1HnC spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=com.zaxxer.hikari.HikariDataSource ## Hikari 连接池配置 ------ 详细配置请访问:https://github.com/brettwooldridge/HikariCP ## 最小空闲连接数量 spring.datasource.hikari.minimum-idle=10 ## 空闲连接存活最大时间,默认600000(10分钟) spring.datasource.hikari.idle-timeout=180000 ## 连接池最大连接数,默认是10 spring.datasource.hikari.maximum-pool-size=50 ## 此属性控制从池返回的连接的默认自动提交行为,默认值:true spring.datasource.hikari.auto-commit=true ## 连接池母子 spring.datasource.hikari.pool-name=MyHikariCP ## 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟 spring.datasource.hikari.max-lifetime=1800000 ## 数据库连接超时时间,默认30秒,即30000 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.connection-test-query=SELECT 1 #==================应用相关配置============= #是否开启请求限制:true->开启,false->关闭 app.config.core.reqLimitEnabled=true #请求限制:每秒允许最大并发限制 app.config.core.reqLimitMax=300 #=======单机模式========== spring.redis.database=0 # Redis服务器地址(单机模式) spring.redis.host=redis01.io # Redis服务器连接端口 spring.redis.port=16579 # Redis服务器连接密码(默认为空) spring.redis.password=ZvsXBp2uyoqpcH5M # 连接超时时间(毫秒) spring.redis.timeout=20000 #=======连接池========== # 连接池最大连接数(使用负值表示没有限制),如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽) spring.redis.jedis.pool.max-active=200 # 连接池中的最大空闲连接,控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8 spring.redis.jedis.pool.max-idle=50 #========安全配置============ #签名算法: #
  • 1:请求参数串=请求报文体中 parameter 对象转换为 json字符串(统一用fastjson)
  • #
  • 2:将应用密钥分别添加到 请求参数串+随机数+时间戳 的头部和尾部:secret+请求参数字符串+随机数+时间戳+secret
  • #
  • 3:对该字符串进行 SHA256 运算,得到一个byte数组
  • #
  • 4:将该byte数组转换为十六进制的字符串,该字符串即是这些请求参数对应的签名
  • #
  • 5:HEX(SHA256(secret+请求参数字符串+随机数+时间戳+secret))
  • #渠道_CHENNEL_1:secretKey->安全密钥,appKey->渠道编码,sign->是否需要签名,true->需要签名 app.config.core.securityMap[CHENNEL_1].secretKey=5c06aedadb259698dc59f64fc02f4488d32fb2fd298d156873e23ed37311a2b600000018 app.config.core.securityMap[CHENNEL_1].appKey=CHENNEL_1 app.config.core.securityMap[CHENNEL_1].sign=true #========接口业务数据加解密配置============ #加密算法:Base64(DES(value,secretKey)) #解密 #格式=> app.config.decode.decodeParamMap[(版本号_)接口名称]=['解密参数1'],['解密参数2']... app.config.decode.decodeParamMap[getExampleInfo]=['id'] #加密 #格式=> encode.encodeParamMap[(版本号_)接口名称]=['编码参数名1'],['编码参数名2']... app.config.encode.encodeParamMap[getExampleInfo]=['id'],['data'] #========接口加锁配置============ #说明:参数为空时为方法级加锁,否则是参数级加锁 #格式=> app.config.lock.lockParamMap[(版本号_)接口名称].secondTimeout=60 必须 #格式=> app.config.lock.lockParamMap[(版本号_)接口名称].params=['参数名1'],['参数名2']... app.config.lock.lockParamMap[getExampleInfo].secondTimeout=10 app.config.lock.lockParamMap[getExampleInfo].params=['id'] #========日志配置============ app.config.log.impl=example #========授权配置============ app.config.auth.impl=example ``` ## 插件开发指南 ##### XService的插件机制: 基于xkernel 提供的SPI机制,结合SpringBoot注解 ConditionalOnBean,ConditionalOnProperty实现, ##### XService的扩展点: - 授权服务:com.javacoo.xservice.base.support.auth.AuthService - 服务日志记录服务:com.javacoo.xservice.base.support.log.LogService - 表达式解析:com.javacoo.xservice.base.support.expression.ExpressionParser - 分布式锁:com.javacoo.xservice.base.support.lock.Lock 系统提供默认实现: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0316/162632_cf9c255e_121703.png "ext.png") xkernel spi 开发步骤及实现机制 见:https://gitee.com/javacoo/xkernel ##### XService插件开发示例:授权服务扩展 - 第一步实现授权服务接口:AuthServiceImpl ```java /** * 授权服务实现 *
  • * * @author: duanyong@jccfc.com * @since: 2021/3/3 13:40 */ @Slf4j public class AuthServiceImpl implements AuthService { /** * 授权 *
  • * * @param o : 参数 * @author duanyong@jccfc.com * @date 2021/3/2 18:11 * @return: void true-> 成功 */ @Override public boolean auth(Object o) { log.info("授权:{}", o); return true; } } ``` - 第二步编写注册文件:在工程类路径下新建META-INF/services目录,新建com.javacoo.xservice.base.support.auth.AuthService文件,内容如下: ```text example=AuthServiceImpl ``` - 第三步修改配置文件 ```properties #========授权配置============ app.config.auth.impl=example ``` ## 软件架构 ![总体逻辑架构](https://images.gitee.com/uploads/images/2020/0528/142050_74d5406e_121703.png "屏幕截图.png") 快速开发框架基于SpringBoot2.X实现。具体分为:终端展现层、网关层、应用层、数据层。 1. 终端展现层:终端分为电视端,微信端,PC浏览器。 2. 网关层:基于Kong,实现了服务注册,流量控制,负载均衡,签名验签等。 3. 应用层:转发展现层、远程调用等对业务层的逻辑请求,由控制层完成请求的接入、参数校验、流转调度。将所有请求接入后统一转交给业务集成层完成具体的服务调用。 4. 业务层:所有的业务逻辑代码都集中在这一层,包括本地业务,和远程服务。 5. 数据层:提供访问数据库,缓存组件,统一了数据访问接口。 #### 框架设计 ###### 概述 - 框架的Web接入控制层基于SpringBoot实现,充分利用SpringBoot提供的拦截器架构,对请求的接入和相关控制提供可拨插式的透明、松耦合的服务。 框架提供了统一通用的Controller实现类BaseController。BaseController提供了统一的异常处理,响应数据处理等。 - 同时框架也对请求和响应数据提供了基类型,它们分别是:BaseRequest和BaseResponse,并抽象了常用请求参数BaseParameter,统一了接口请求和响应的报文规范。 - 除基本的Controller和DataBean外,框架提供了JSON请求数据转换拦截器、动态DataBean对象绑定拦截器、系统安全拦截器、加解密,签名验签等功能组件。具体内容详见后面描述。 - 插件体系设计:框架基于xkernel 提供的SPI机制,结合SpringBoot提供的 ConditionalOnBean,ConditionalOnProperty等注解,实现了灵活可扩展的插件体系。 ###### 框架Controller体系结构 类结构模型:带参数 ![带参数](https://images.gitee.com/uploads/images/2020/0528/143348_b7448fd9_121703.png "屏幕截图.png") 类结构模型:不带参数 ![不带参数](https://images.gitee.com/uploads/images/2020/0528/143420_8e980347_121703.png "屏幕截图.png") - Controller中的handle方法,接口处理入口方法,@RequestMapping注解,规定了整个业务处理流程:请求参数解析->检查请求参数->业务处理->设置响应数据。 - Controller中的execute方法,执行具体业务流程,使用了熔断机制。 - Controller中的parse方法,解析请求参数,将请求参数中的业务参数对象转换为服务使用的对象。 - Controller中的initBaseParameter,初始化初始请求参数:比如获取IP,MAC等信息。 - Controller中的validate校验请求中的业务参数,由子类实现,如果参数检查不通过,请抛出参数异常:IllegalParameterException,由具体子类实现。 - Controller中的process业务方法调用,由具体子类实现。 - Controller中的getParamClass获取参数类型,由具体子类实现。 - Controller中的fallback 服务降级,默认返回REQUEST_TIMEOUT字符串,框架统一处理抛出TimeoutException异常。 - Controller中的checkAuth授权校验。 ###### 拦截器体系结构 类结构模型 ![类结构模型](https://images.gitee.com/uploads/images/2020/0528/144410_44b91672_121703.png "屏幕截图.png") - BaseInterceptor拦截器基类,继承自HandlerInterceptorAdapter,覆盖了preHandle,postHandle,afterCompletion,afterConcurrentHandlingStarted方法,通过拦截器数组,实现拦截器链效果。 - HandlerInterceptor拦截器,继承自BaseInterceptor,基于HandlerStack处理器链组件实现了整个业务处理核心流程: 1.preHandle预处理流程包括: a)初始化数据交换区:基于ThreadLocal实现,封装了此次请求的相关信息SwapAreaData,供整个请求过程中使用。 b)解析请求参数:将原始请求参数转换为框架内部BaseRequest对象 c)执行预处理流程:生成全局流水号,依次执行注册的预处理器HandlerStack,如参数解码,参数校验等。 2.postHandle提交处理流程包括: a)依次执行注册的预处理器HandlerStack,如编码,签名等。 b)设置响应数据:响应数据转换为目标格式(如JSON格式) 3.afterCompletion完成处理后续流程包括: a)异步发布交易完成事件。 b)释放当前线程数据交换区数据 - LocaleInterceptor国际化信息设置(基于SESSION)拦截器,继承自BaseInterceptor,基于HandlerStack处理器链组件实现了整个业务处理核心流程: 1.preHandle预处理流程包括: a)设置客户端语言。 - MaliciousRequestInterceptor恶意请求拦截器,继承自BaseInterceptor,基于HandlerStack处理器链组件实现了整个业务处理核心流程: 1.preHandle预处理流程包括: a)根据配置参数,处理请求,拦截恶意请求。 - RequestLimitInterceptor请求流量限制拦截器,继承自BaseInterceptor,基于HandlerStack处理器链组件实现了整个业务处理核心流程: 1.preHandle预处理流程包括: a)根据配置参数,限制处理请求数量。 ###### 异常体系结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0528/151606_eceb78e5_121703.png "屏幕截图.png") - BaseException框架异常基类,继承自RuntimeException - BusinessException业务类异常,继承自BaseException - DataParseException数据解析类异常,继承自BaseException - IllegalParameterException请求参数类异常,继承自BaseException - RemoteException远程接口调用类异常,继承自BaseException - ServiceException服务内部异常,继承自BaseException - TimeoutException请求超时异常,继承自BaseException ###### 过滤器体系结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0528/151914_c17aa71a_121703.png "屏幕截图.png") - CorsFilter跨域请求过滤器 - CsrfFilter跨站请求伪造攻击过滤器 - XssFilter非法字符过滤器(防SQL注入,防XSS漏洞) ###### 授权组件结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0316/162600_5ff6b68e_121703.png "AuthService.png") - auth校验渠道是否已经授权 ###### 事件体系结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0528/152229_39eec9f1_121703.png "屏幕截图.png") - TransCompleteEvent交易完成事件对象,基于Spring的事件机制。 - ApplicationContextProvider applicationContext提供者。 - AsyncApplicationEventMulticaster 异步事件处理,为了实现异步事件处理,这里需要重新实现SimpleApplicationEventMulticaste ###### 处理器体系结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0309/142036_561bd413_121703.png "Handler.png") - Handler处理器接口。 - EnCodeHandler 编码处理器。 - DeCodeHandler 解码处理器。 - ParamValidatorHandler参数校验處理器。 - ResponseSignHandler返回数据签名处理器。 - MethodLockHandler加锁处理器。 - MethodUnLockHandler解锁处理器。 - HandlerStack处理器链组件接口。 - DefaultHandlerStack 默认处理器链实现 ###### 熔断处理组件结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0528/152604_31689217_121703.png "屏幕截图.png") - HystrixUtil Hystrix工具类。 - InvokeTimeoutNonParamHystrixCommand无参数调用超时Command。 - InvokeTimeoutParamHystrixCommand 带参数调用超时Comman ###### 数据交换区组件结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0528/154133_b2beb69d_121703.png "屏幕截图.png") - SwapArea 内部数据交换区。 - SwapAreaHolder 内部数据交换区Holder。 - SwapAreaManager数据交换区管理接口。 - DefaultSwapArea默认数据交换区实现。 - DefaultSwapAreaManager 默认数据交换区管理器。 - ThreadLocalSwapAreaHolder 基于ThreadLoca实现数据交换区Holder。 - SwapAreaUtils数据交换工具类。 ###### 统一异常处理组件结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0528/154229_4da340b1_121703.png "屏幕截图.png") - exceptionHandler根据配置统一处理异常信息 ##### 表达式处理组件结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0316/162059_78fcf4fc_121703.png "Expression.png") - Expression parseExpression(String el) 根据指定的表达式获取表达式对象 - Object getValue(String el, Object root) 根据指定的表达式从上下文中取值 - T getValue(String el, Object root, Class clazz) 根据指定的表达式和目标数据类型从上下文中取值 - void setValue(String el, Object value, Object root) 根据指定的表达式将值设置到上下文中 ##### 分布式锁处理组件结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0316/162113_10ca0af3_121703.png "Lock.png") - T lock(String lockKey) 对lockKey加锁 - T lock(String lockKey, int timeout) 对lockKey加锁,timeout后过期 - T lock(String lockKey, TimeUnit unit , int timeout) 对lockKey加锁,指定时间单位,timeout后过期 - boolean tryLock(String lockKey) 尝试获取锁 - boolean tryLock(String lockKey, int waitTime, int timeout) 尝试获取锁 - boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int timeout) 尝试获取锁 - void unlock(String lockKey) 释放锁 - void unlock(T lock) 释放锁 ##### 服务日志处理组件结构 类结构模型 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0316/162131_38060899_121703.png "LogService.png") - default void record(SwapAreaData logData) 记录日志 #### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request #### 码云特技 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 2. 码云官方博客 [blog.gitee.com](https://blog.gitee.com) 3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解码云上的优秀开源项目 4. [GVP](https://gitee.com/gvp) 全称是码云最有价值开源项目,是码云综合评定出的优秀开源项目 5. 码云官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) 6. 码云封面人物是一档用来展示码云会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)