XService接口服务快速开发框架,基于SpringBoot实现,封装了接口开发过程中的基础功能及控制流程,并约定了统一的接口报文格式,制定了完善的开发规范以及测试规范,让程序员只需关注具体业务实现,提高了开发接口服务的效率。
XService基础功能基于xkernel 提供的SPI机制,结合SpringBoot提供的 ConditionalOnBean,ConditionalOnProperty等注解实现,实用,简单,扩展灵活。
如果你是使用Maven
来构建项目,你需要添加XService的pom.xml
文件内,如下所示:
<dependency> <groupId>com.javacoo.xservice</groupId> <artifactId>xservice_base</artifactId> <version>1.0.0</version> </dependency>
添加完组件我们就可以进行配置使用了。
请求协议公共部分
参数 | 类型 | 是否必选 | 描述 |
---|---|---|---|
appKey | String | 是 | 应用key |
nonce | String | 是 | 32位UUID随机字串 |
sign | String | 是 | 签名 |
timestamp | Long | 是 | 请求时间戳,防止重放攻击 |
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:参数字符串=将报文体中 业务对象 转换为 json字符串。
2:将应用密钥(secretKey)分别添加到 参数字符串+随机数+时间戳 的头部和尾部:secretKey+参数字符串+随机数+时间戳+secretKey.
3:对该字符串进行 SHA256 运算,得到一个byte数组。
4:将该byte数组转换为十六进制的字符串,该字符串即是签名。
加密/解密
加密算法:Base64(DES(value,secretKey))
解密算法:DES(Base64(value),secretKey)
定义接口服务
XService约定一个接口服务为一个Controller类,且此类必须继承框架提供的三个基类之一。
带参数的接口服务基类:AbstractParamController
/** * 业务参数控制器基类 * <p>说明:</p> * <li>定义有业务参数的接口处理基本流程</li> * @author DuanYong * @param <P> 参数 * @since 2017年6月28日下午2:48:27 */ @Slf4j public abstract class AbstractParamController<P extends BaseParameter> extends BaseController { /** * 接口处理 * <p>说明:</p> * <li>1:请求参数解析</li> * <li>2:检查请求参数</li> * <li>3:业务处理</li> * <li>4:设置响应数据</li> * @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()); } /** * 执行 * <p>说明:</p> * <li>hystrix</li> * @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); } /** * 解析请求参数 * <p>说明:</p> * <li>将请求参数中的业务参数对象转换为服务使用的对象</li> * @author DuanYong * @since 2017年6月28日下午3:17:32 * @return: java.util.Optional<P> 业务参数对象 */ protected final Optional<P> 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)); } } /** * 初始化初始请求参数 * <p>说明:</p> * <li>解析并初始化请求参数对象</li> * @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; } /** * 校验请求中的业务参数 * <p>说明:</p> * <li>由子类实现,如果参数检查不通过,请抛出参数异常:IllegalParameterException</li> * @author DuanYong * @param p 业务参数对象 * @throws IllegalParameterException * @since 2017年6月28日下午2:28:10 */ protected abstract void validate(P p) throws IllegalParameterException; /** * 具体业务处理 * <p>说明:</p> * <li>由子类实现</li> * @author DuanYong * @param p 业务参数对象 * @return 业务返回数据 * @since 2017年5月5日下午3:24:09 */ protected abstract Object process(P p); /** * 获取参数类型 * <p>说明:</p> * <li></li> * @author DuanYong * @return 参数类型对象 * @since 2017年7月24日上午10:33:30 */ protected abstract Class<P> getParamClass(); /** * 服务降级,默认返回REQUEST_TIMEOUT字符串,框架统一处理抛出TimeoutException异常 * <p>说明:</p> * <li>注意:在fallback方法中不允许有远程方法调用,方法尽量要轻,调用其他外部接口也要进行hystrix降级。否则执行fallback方法会抛出异常</li> * @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<P,P> validateFunction = (P p)->{ validate(p); return p; }; /** * 执行业务处理 */ private Function<P,Object> executeFunction = (P p)-> process(p); /** * 执行降级业务处理 */ private Function<P,Object> fallbackFunction = (P p)-> fallback(p); }
无参数的接口服务基类:AbstractNonParamController
/** * 无业务参数控制器基类 * <p>说明:</p> * <li>定义无业务参数接口处理基本流程</li> * <li>统一异常处理</li> * @author DuanYong * @since 2017年7月11日上午8:49:58 */@Slf4jpublic abstract class AbstractNonParamController extends BaseController { /** * 具体业务处理 * <p>说明:</p> * <li>由子类实现</li> * @author DuanYong * @return 业务返回数据 * @since 2017年7月11日上午8:51:23 */ protected abstract Object process(); /** * 接口处理 * <p>说明:</p> * <li>业务处理</li> * <li>设置响应数据</li> * @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()); } /** * 执行 * <p>说明:</p> * @author DuanYong * @return: java.lang.Object 业务返回数据 * @since 2017年11月13日下午3:41:04 */ private final Object execute(){ return executeFunction.get(); } /** * 执行业务处理 */ private Supplier<Object> executeFunction = ()-> process(); }
url参数接口服务基类:AbstractUrlParamController
/** * 业务参数控制器基类 * <p>说明:</p> * <li>定义有业务参数的接口处理基本流程</li> * @author DuanYong * @since 2017年6月28日下午2:48:27 */@Slf4jpublic abstract class AbstractUrlParamController extends BaseController { /** * 接口处理 * <p>说明:</p> * <li>1:请求参数解析</li> * <li>2:检查请求参数</li> * <li>3:业务处理</li> * <li>4:设置响应数据</li> * @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()); } /** * 执行 * <p>说明:</p> * @author DuanYong * @param p 请求参数 * @return Object 业务返回数据 * @since 2017年11月13日下午3:41:04 */ private final Object execute(Map<String,String> p){ return executeFunction.apply(p); } /** * 解析请求参数 * <p>说明:</p> * <li>将URL请求参数中的业务参数对象转换为服务使用的MAP对象</li> * @author DuanYong * @since 2017年6月28日下午3:17:32 * @return: java.util.Optional<Map<String,String>> 业务参数对象 */ protected final Optional<Map<String,String>> 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()); } /** * 校验请求中的业务参数 * <p>说明:</p> * <li>由子类实现,如果参数检查不通过,请抛出参数异常:IllegalParameterException</li> * @author DuanYong * @param p 业务参数对象 * @throws IllegalParameterException * @since 2017年6月28日下午2:28:10 */ protected abstract void validate(Map<String,String> p) throws IllegalParameterException; /** * 具体业务处理 * <p>说明:</p> * <li>由子类实现</li> * @author DuanYong * @param p 业务参数对象 * @return 业务返回数据 * @since 2017年5月5日下午3:24:09 */ protected abstract Object process(Map<String,String> p); /** * 校验并返回业务参数 */ private Function<Map<String,String>,Map<String,String>> validateFunction = (Map<String,String> p)->{ validate(p); return p; }; /** * 执行业务处理 */ private Function<Map<String,String>,Object> executeFunction = (Map<String,String> p)-> process(p);}
其他约定:
服务开发过程中尽量少使用多线程,如果使用了多线程,框架提供的交换区对象(ThreadLocal实现)将无法正常使用。
打印日志请使用LogUtil中的方法,因为框架对日志输出进行了增强,统一添加了流水号(基于交换区对象)。
LogUtil只能在当前主线程下使用。
定义业务接口协议:协议公共部分+协议业务部分
请求协议业务部分如下:parameter 对象
参数 | 类型 | 是否必选 | 描述 |
---|---|---|---|
id | String | 是 | 业务主键 |
响应协议业务部分如下:data 对象
参数 | 类型 | 描述 |
---|---|---|
id | Integer | 主键 |
data | String | 数据 |
编写实现代码
获取案例数据接口,带参数Controller:ExampleController
/** * 获取案例数据接口,带参数 * <p>说明:</p> * <li></li> * @author DuanYong * @since 2017年7月17日上午9:02:56 */@Slf4j@RestController@RequestMapping(value = "/example/v1/getExampleInfo")public class ExampleController extends AbstractParamController<BaseReq> { /** 数据服务 */ @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<BaseReq> getParamClass() { return BaseReq.class; }}
获取案例数据接口,无参数Controller:ExampleNonParamController
/** * 获取案例数据接口,无参数 * <p>说明:</p> * <li></li> * @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
/** * 获取案例数据接口,url参数 * <p>说明:</p> * <li></li> * @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<String, String> p) throws IllegalParameterException { log.info("validate->{}",p); } @Override protected Object process(Map<String, String> p) { return exampleService.getExampleInfo(p.get("id")); }}
请求业务对象:BaseReq
/** * 查询对象基类 * <p>说明:</p> * <li>定义相关公共查询字段</li> * @author DuanYong * @since 2017年7月17日上午8:55:10 */@Data@AllArgsConstructor@NoArgsConstructorpublic class BaseReq extends BaseParameter { /** * ID */ private String id;}
响应业务对象:ExampleDto
/** * 参数 * <p>说明:</p> * <li></li> * @author DuanYong * @since 2017年7月14日下午1:04:59 */@Data@AllArgsConstructor@NoArgsConstructorpublic class ExampleDto { /** * id */ private String id; /** * 数据 */ private String data;}
编写:ExampleDao及ExampleDaoMapper.xml
/** * Example服务DAO * <p>说明:</p> * <li></li> * @author DuanYong * @since 2017年7月14日下午1:37:04 */public interface ExampleDao { /** * 根据版块ID ,查询版块内容 * <p>说明:</p> * <li></li> * @author DuanYong * @param id * @return ExampleDto * @since 2017年7月14日下午1:40:26 */ ExampleDto getExampleInfo(@Param("id")String id);}
ExampleDaoMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.javacoo.xservice.example.dao.ExampleDao"> <!-- 案例 --> <select id="getExampleInfo" resultType="com.javacoo.xservice.example.bean.dto.ExampleDto"> SELECT id,data FROM example WHERE id=#{id} </select></mapper>
定义服务接口:ExampleService
/** * 案例数据服务接口 * <p>说明:</p> * <li>获取详细数据</li> * @author DuanYong * @since 2017年7月14日上午10:54:21 */public interface ExampleService { /** * 获取版块及版块下内容信息 * <p>说明:</p> * <li></li> * @author DuanYong * @param id 参数 * @return * @since 2017年7月14日上午11:23:21 */ Optional<ExampleDto> getExampleInfo(String id);}
实现服务:ExampleServiceImpl
/** * 案例数据服务接口实现 * <p>说明:</p> * <li></li> * @author DuanYong * @since 2017年7月14日下午1:30:18 */@Service@Slf4jpublic class ExampleServiceImpl implements ExampleService { @Autowired private ExampleDao exampleDao; @Override public Optional<ExampleDto> getExampleInfo(String id) { AbstractAssert.isNotBlank(id, ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID); return Optional.ofNullable(exampleDao.getExampleInfo(id)); }}
编写测试
测试基类
/** * 测试基类 * <li></li> * @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(); } /** * 校验结果 * <li></li> * @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
/** * 接口测试 * <li></li> * * @author: duanyong@jccfc.com * @since: 2021/3/3 13:49 */@Slf4jpublic 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; }}
测试日志
[ 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 : 返回数据合法
配置及说明
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 #========安全配置============ #签名算法: #<li>1:请求参数串=请求报文体中 parameter 对象转换为 json字符串(统一用fastjson)</li> #<li>2:将应用密钥分别添加到 请求参数串+随机数+时间戳 的头部和尾部:secret+请求参数字符串+随机数+时间戳+secret</li> #<li>3:对该字符串进行 SHA256 运算,得到一个byte数组</li> #<li>4:将该byte数组转换为十六进制的字符串,该字符串即是这些请求参数对应的签名</li> #<li>5:HEX(SHA256(secret+请求参数字符串+随机数+时间戳+secret))</li> #渠道_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</li> 必须 #格式=> app.config.lock.lockParamMap[(版本号_)接口名称].params=['参数名1'],['参数名2']...</li> app.config.lock.lockParamMap[getExampleInfo].secondTimeout=10 app.config.lock.lockParamMap[getExampleInfo].params=['id'] #========日志配置============ app.config.log.impl=example #========授权配置============ app.config.auth.impl=example
基于xkernel 提供的SPI机制,结合SpringBoot注解 ConditionalOnBean,ConditionalOnProperty实现,
授权服务: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
系统提供默认实现:
ext.png
xkernel spi 开发步骤及实现机制 见:https://gitee.com/javacoo/xkernel
第一步实现授权服务接口:com.javacoo.xservice.example.service.impl.AuthServiceImpl
/** * 授权服务实现 * <li></li> * * @author: duanyong@jccfc.com * @since: 2021/3/3 13:40 */@Slf4jpublic class AuthServiceImpl implements AuthService { /** * 授权 * <li></li> * * @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文件,内容如下:
example=com.javacoo.xservice.example.service.impl.AuthServiceImpl
第三步修改配置文件
#========授权配置============ app.config.auth.impl=example
总体逻辑架构
快速开发框架基于SpringBoot2.X实现。具体分为:终端展现层、网关层、应用层、数据层。
终端展现层:终端分为电视端,微信端,PC浏览器。
网关层:基于Kong,实现了服务注册,流量控制,负载均衡,签名验签等。
应用层:转发展现层、远程调用等对业务层的逻辑请求,由控制层完成请求的接入、参数校验、流转调度。将所有请求接入后统一转交给业务集成层完成具体的服务调用。
业务层:所有的业务逻辑代码都集中在这一层,包括本地业务,和远程服务。
数据层:提供访问数据库,缓存组件,统一了数据访问接口。
框架的Web接入控制层基于SpringBoot实现,充分利用SpringBoot提供的拦截器架构,对请求的接入和相关控制提供可拨插式的透明、松耦合的服务。
框架提供了统一通用的Controller实现类BaseController。BaseController提供了统一的异常处理,响应数据处理等。
同时框架也对请求和响应数据提供了基类型,它们分别是:BaseRequest和BaseResponse,并抽象了常用请求参数BaseParameter,统一了接口请求和响应的报文规范。
除基本的Controller和DataBean外,框架提供了JSON请求数据转换拦截器、动态DataBean对象绑定拦截器、系统安全拦截器、加解密,签名验签等功能组件。具体内容详见后面描述。
插件体系设计:框架基于xkernel 提供的SPI机制,结合SpringBoot提供的 ConditionalOnBean,ConditionalOnProperty等注解,实现了灵活可扩展的插件体系。
类结构模型:带参数
带参数
类结构模型:不带参数
不带参数
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授权校验。
类结构模型
类结构模型
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)根据配置参数,限制处理请求数量。
类结构模型
输入图片说明
BaseException框架异常基类,继承自RuntimeException
BusinessException业务类异常,继承自BaseException
DataParseException数据解析类异常,继承自BaseException
IllegalParameterException请求参数类异常,继承自BaseException
RemoteException远程接口调用类异常,继承自BaseException
ServiceException服务内部异常,继承自BaseException
TimeoutException请求超时异常,继承自BaseException
类结构模型
输入图片说明
CorsFilter跨域请求过滤器
CsrfFilter跨站请求伪造攻击过滤器
XssFilter非法字符过滤器(防SQL注入,防XSS漏洞)
类结构模型
AuthService.png
auth校验渠道是否已经授权
类结构模型
输入图片说明
TransCompleteEvent交易完成事件对象,基于Spring的事件机制。
ApplicationContextProvider applicationContext提供者。
AsyncApplicationEventMulticaster 异步事件处理,为了实现异步事件处理,这里需要重新实现SimpleApplicationEventMulticaste
类结构模型
输入图片说明
Handler处理器接口。
EnCodeHandler 编码处理器。
DeCodeHandler 解码处理器。
ParamValidatorHandler参数校验處理器。
ResponseSignHandler返回数据签名处理器。
MethodLockHandler加锁处理器。
MethodUnLockHandler解锁处理器。
HandlerStack处理器链组件接口。
DefaultHandlerStack 默认处理器链实现
类结构模型
输入图片说明
HystrixUtil Hystrix工具类。
InvokeTimeoutNonParamHystrixCommand无参数调用超时Command。
InvokeTimeoutParamHystrixCommand 带参数调用超时Comman
类结构模型
输入图片说明
SwapArea 内部数据交换区。
SwapAreaHolder 内部数据交换区Holder。
SwapAreaManager数据交换区管理接口。
DefaultSwapArea默认数据交换区实现。
DefaultSwapAreaManager 默认数据交换区管理器。
ThreadLocalSwapAreaHolder 基于ThreadLoca实现数据交换区Holder。
SwapAreaUtils数据交换工具类。
类结构模型
输入图片说明
exceptionHandler根据配置统一处理异常信息
类结构模型
输入图片说明
Expression parseExpression(String el) 根据指定的表达式获取表达式对象
Object getValue(String el, Object root) 根据指定的表达式从上下文中取值
<T> T getValue(String el, Object root, Class<T> clazz) 根据指定的表达式和目标数据类型从上下文中取值
void setValue(String el, Object value, Object root) 根据指定的表达式将值设置到上下文中
类结构模型
输入图片说明
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) 释放锁
类结构模型
输入图片说明
default void record(SwapAreaData logData) 记录日志
https://gitee.com/javacoo/xService
2023-12-13T11:46:19
2023-12-13T11:48:22
2024-01-02T09:07:42
2024-01-02T09:07:20
2024-01-02T09:06:50
2024-01-02T09:06:26
2024-01-02T09:06:01
2024-01-02T09:05:20
2024-01-02T09:04:49
2024-01-02T09:04:17