XService:為組件化,快速構(gòu)建微服務而生
什么是XService?
XService接口服務快速開發(fā)框架,基于SpringBoot實現(xiàn)竭讳,封裝了接口開發(fā)過程中的基礎(chǔ)功能及控制流程,并約定了統(tǒng)一的接口報文格式戒努,制定了完善的開發(fā)規(guī)范以及測試規(guī)范搂捧,讓程序員只需關(guān)注具體業(yè)務實現(xiàn)浑此,提高了開發(fā)接口服務的效率蚯妇。
XService基礎(chǔ)功能基于xkernel 提供的SPI機制是趴,結(jié)合SpringBoot提供的 ConditionalOnBean,ConditionalOnProperty等注解實現(xiàn)涛舍,實用,簡單唆途,擴展靈活富雅。
安裝 & 入門
如果你是使用Maven
來構(gòu)建項目,你需要添加XService的pom.xml
文件內(nèi)肛搬,如下所示:
<dependency>
<groupId>com.javacoo.xservice</groupId>
<artifactId>xservice_base</artifactId>
<version>1.0.0</version>
</dependency>
添加完組件我們就可以進行配置使用了没佑。
使用指南
XService的約定
- 請求協(xié)議公共部分
參數(shù) | 類型 | 是否必選 | 描述 |
---|---|---|---|
appKey | String | 是 | 應用key |
nonce | String | 是 | 32位UUID隨機字串 |
sign | String | 是 | 簽名 |
timestamp | Long | 是 | 請求時間戳,防止重放攻擊 |
transactionSn | String | 是 | 交易流水號 |
parameter | Object | 否 | 請求的業(yè)務對象 |
- 響應協(xié)議公共部分
參數(shù) | 類型 | 描述 |
---|---|---|
code | String | 返回碼,具體含有參見下文 |
message | String | 返回消息温赔,如錯誤信息 |
timestamp | String | 響應時間 |
transactionSn | String | 交易流水號 |
sign | String | 簽名 |
data | Object | 返回的業(yè)務對象 |
- 返回碼
平臺級返回碼如下:業(yè)務可制定具體的業(yè)務返回代碼
code | message | 說明 |
---|---|---|
200 | 請求成功 | |
207 | 頻繁操作 | 頻繁操作 |
400 | 請求參數(shù)出錯 | 終端傳遞的參數(shù)值錯誤 |
403 | 沒有權(quán)限 | 沒有權(quán)限 |
404 | 服務不存在 | 服務不存在 |
408 | 請求超時 | 請求超時 |
409 | 業(yè)務邏輯出錯 | 服務端執(zhí)行服務方法時 執(zhí)行業(yè)務邏輯校驗出錯,或者響應數(shù)據(jù)為空蛤奢。 |
500 | 系統(tǒng)繁忙,請稍后再試 | 數(shù)據(jù)不滿足提交條件或 服務端執(zhí)行服務方法時出現(xiàn)異常,需由服務人員解決 |
- 簽名/驗簽
簽名算法:HEX(SHA256(secretKey+參數(shù)字符串+隨機數(shù)+時間戳+secretKey))
說明:
1:參數(shù)字符串=將報文體中 業(yè)務對象 轉(zhuǎn)換為 json字符串。
2:將應用密鑰(secretKey)分別添加到 參數(shù)字符串+隨機數(shù)+時間戳 的頭部和尾部:secretKey+參數(shù)字符串+隨機數(shù)+時間戳+secretKey.
3:對該字符串進行 SHA256 運算啤贩,得到一個byte數(shù)組待秃。
4:將該byte數(shù)組轉(zhuǎn)換為十六進制的字符串,該字符串即是簽名痹屹。
-
加密/解密
加密算法:Base64(DES(value,secretKey))
解密算法:DES(Base64(value),secretKey)
-
定義接口服務
XService約定一個接口服務為一個Controller類章郁,且此類必須繼承框架提供的三個基類之一。
帶參數(shù)的接口服務基類:AbstractParamController
/**
* 業(yè)務參數(shù)控制器基類
* <p>說明:</p>
* <li>定義有業(yè)務參數(shù)的接口處理基本流程</li>
* @author DuanYong
* @param <P> 參數(shù)
* @since 2017年6月28日下午2:48:27
*/
@Slf4j
public abstract class AbstractParamController<P extends BaseParameter> extends BaseController {
/**
* 接口處理
* <p>說明:</p>
* <li>1:請求參數(shù)解析</li>
* <li>2:檢查請求參數(shù)</li>
* <li>3:業(yè)務處理</li>
* <li>4:設置響應數(shù)據(jù)</li>
* @author DuanYong
* @since 2017年6月28日下午3:19:43
* @param response 響應對象
*/
@RequestMapping
public final void handle(HttpServletResponse response) {
final Long startTime = System.currentTimeMillis();
//參數(shù)解析->檢查請求參數(shù)->業(yè)務處理->設置響應數(shù)據(jù)
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());
}
/**
* 執(zhí)行
* <p>說明:</p>
* <li>hystrix</li>
* @author DuanYong
* @since 2017年11月13日下午3:41:04
* @param p 業(yè)務參數(shù)
* @return: java.lang.Object 業(yè)務返回對象
*/
private final Object execute(P p){
return executeFunction.apply(p);
}
/**
* 解析請求參數(shù)
* <p>說明:</p>
* <li>將請求參數(shù)中的業(yè)務參數(shù)對象轉(zhuǎn)換為服務使用的對象</li>
* @author DuanYong
* @since 2017年6月28日下午3:17:32
* @return: java.util.Optional<P> 業(yè)務參數(shù)對象
*/
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("將請求參數(shù)中的業(yè)務參數(shù)對象轉(zhuǎn)換為服務使用的對象失敗,流水號:{},請求參數(shù):{},異常信息:", WebUtil.getSwapAreaData().getTransactionSn(),baseRequest.getParameter(),ex);
throw new IllegalParameterException(Resources.getMessage(ErrorCodeConstants.COMMON_REQ_PARAM_PARSE_ERROR));
}
}
/**
* 初始化初始請求參數(shù)
* <p>說明:</p>
* <li>解析并初始化請求參數(shù)對象</li>
* @author DuanYong
* @param paramString 參數(shù)原始json字符串
* @param baseRequest 請求參數(shù)對象
* @return P 業(yè)務參數(shù)對象
* @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;
}
/**
* 校驗請求中的業(yè)務參數(shù)
* <p>說明:</p>
* <li>由子類實現(xiàn)志衍,如果參數(shù)檢查不通過暖庄,請拋出參數(shù)異常:IllegalParameterException</li>
* @author DuanYong
* @param p 業(yè)務參數(shù)對象
* @throws IllegalParameterException
* @since 2017年6月28日下午2:28:10
*/
protected abstract void validate(P p) throws IllegalParameterException;
/**
* 具體業(yè)務處理
* <p>說明:</p>
* <li>由子類實現(xiàn)</li>
* @author DuanYong
* @param p 業(yè)務參數(shù)對象
* @return 業(yè)務返回數(shù)據(jù)
* @since 2017年5月5日下午3:24:09
*/
protected abstract Object process(P p);
/**
* 獲取參數(shù)類型
* <p>說明:</p>
* <li></li>
* @author DuanYong
* @return 參數(shù)類型對象
* @since 2017年7月24日上午10:33:30
*/
protected abstract Class<P> getParamClass();
/**
* 服務降級,默認返回REQUEST_TIMEOUT字符串楼肪,框架統(tǒng)一處理拋出TimeoutException異常
* <p>說明:</p>
* <li>注意:在fallback方法中不允許有遠程方法調(diào)用雄驹,方法盡量要輕,調(diào)用其他外部接口也要進行hystrix降級淹辞。否則執(zhí)行fallback方法會拋出異常</li>
* @author DuanYong
* @param p 參數(shù)
* @return REQUEST_TIMEOUT
* @since 2018年8月21日上午11:20:37
*/
protected Object fallback(P p){
return Constants.REQUEST_TIMEOUT;
}
/**
* 校驗并返回業(yè)務參數(shù)
*/
private Function<P,P> validateFunction = (P p)->{
validate(p);
return p;
};
/**
* 執(zhí)行業(yè)務處理
*/
private Function<P,Object> executeFunction = (P p)-> process(p);
/**
* 執(zhí)行降級業(yè)務處理
*/
private Function<P,Object> fallbackFunction = (P p)-> fallback(p);
}
無參數(shù)的接口服務基類:AbstractNonParamController
/**
* 無業(yè)務參數(shù)控制器基類
* <p>說明:</p>
* <li>定義無業(yè)務參數(shù)接口處理基本流程</li>
* <li>統(tǒng)一異常處理</li>
* @author DuanYong
* @since 2017年7月11日上午8:49:58
*/
@Slf4j
public abstract class AbstractNonParamController extends BaseController {
/**
* 具體業(yè)務處理
* <p>說明:</p>
* <li>由子類實現(xiàn)</li>
* @author DuanYong
* @return 業(yè)務返回數(shù)據(jù)
* @since 2017年7月11日上午8:51:23
*/
protected abstract Object process();
/**
* 接口處理
* <p>說明:</p>
* <li>業(yè)務處理</li>
* <li>設置響應數(shù)據(jù)</li>
* @since 2017年7月11日上午9:13:28
*/
@RequestMapping
private final void handle(HttpServletResponse httpServletResponse) {
Long startTime = System.currentTimeMillis();
//業(yè)務處理->設置響應數(shù)據(jù)
Optional.ofNullable(execute()).map(o->setSuccessResponse(httpServletResponse,o));
log.info("接口->{},處理完成,耗時->{}秒,流水號:{}", SwapAreaUtils.getSwapAreaData().getReqMethod(),(System.currentTimeMillis() - startTime)/1000.0,SwapAreaUtils.getSwapAreaData().getTransactionSn());
}
/**
* 執(zhí)行
* <p>說明:</p>
* @author DuanYong
* @return: java.lang.Object 業(yè)務返回數(shù)據(jù)
* @since 2017年11月13日下午3:41:04
*/
private final Object execute(){
return executeFunction.get();
}
/**
* 執(zhí)行業(yè)務處理
*/
private Supplier<Object> executeFunction = ()-> process();
}
url參數(shù)接口服務基類:AbstractUrlParamController
/**
* 業(yè)務參數(shù)控制器基類
* <p>說明:</p>
* <li>定義有業(yè)務參數(shù)的接口處理基本流程</li>
* @author DuanYong
* @since 2017年6月28日下午2:48:27
*/
@Slf4j
public abstract class AbstractUrlParamController extends BaseController {
/**
* 接口處理
* <p>說明:</p>
* <li>1:請求參數(shù)解析</li>
* <li>2:檢查請求參數(shù)</li>
* <li>3:業(yè)務處理</li>
* <li>4:設置響應數(shù)據(jù)</li>
* @author DuanYong
* @since 2017年6月28日下午3:19:43
*/
@RequestMapping
public final void handle(HttpServletResponse response) {
Long startTime = System.currentTimeMillis();
//參數(shù)解析->檢查請求參數(shù)->業(yè)務處理->設置響應數(shù)據(jù)
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());
}
/**
* 執(zhí)行
* <p>說明:</p>
* @author DuanYong
* @param p 請求參數(shù)
* @return Object 業(yè)務返回數(shù)據(jù)
* @since 2017年11月13日下午3:41:04
*/
private final Object execute(Map<String,String> p){
return executeFunction.apply(p);
}
/**
* 解析請求參數(shù)
* <p>說明:</p>
* <li>將URL請求參數(shù)中的業(yè)務參數(shù)對象轉(zhuǎn)換為服務使用的MAP對象</li>
* @author DuanYong
* @since 2017年6月28日下午3:17:32
* @return: java.util.Optional<Map<String,String>> 業(yè)務參數(shù)對象
*/
protected final Optional<Map<String,String>> parse(){
BaseRequest baseRequest = SwapAreaUtils.getSwapAreaData().getBaseRequest();
if(baseRequest.getQueryStringMap().isEmpty()){
log.error("解析URL請求參數(shù)失敗医舆,請求參數(shù)為空,流水號:{}", WebUtil.getSwapAreaData().getTransactionSn());
throw new IllegalParameterException(Resources.getMessage(ErrorCodeConstants.COMMON_REQ_PARAM_PARSE_IS_EMPTY));
}
return Optional.ofNullable(baseRequest.getQueryStringMap());
}
/**
* 校驗請求中的業(yè)務參數(shù)
* <p>說明:</p>
* <li>由子類實現(xiàn),如果參數(shù)檢查不通過象缀,請拋出參數(shù)異常:IllegalParameterException</li>
* @author DuanYong
* @param p 業(yè)務參數(shù)對象
* @throws IllegalParameterException
* @since 2017年6月28日下午2:28:10
*/
protected abstract void validate(Map<String,String> p) throws IllegalParameterException;
/**
* 具體業(yè)務處理
* <p>說明:</p>
* <li>由子類實現(xiàn)</li>
* @author DuanYong
* @param p 業(yè)務參數(shù)對象
* @return 業(yè)務返回數(shù)據(jù)
* @since 2017年5月5日下午3:24:09
*/
protected abstract Object process(Map<String,String> p);
/**
* 校驗并返回業(yè)務參數(shù)
*/
private Function<Map<String,String>,Map<String,String>> validateFunction = (Map<String,String> p)->{
validate(p);
return p;
};
/**
* 執(zhí)行業(yè)務處理
*/
private Function<Map<String,String>,Object> executeFunction = (Map<String,String> p)-> process(p);
}
- 其他約定:
服務開發(fā)過程中盡量少使用多線程蔬将,如果使用了多線程,框架提供的交換區(qū)對象(ThreadLocal實現(xiàn))將無法正常使用央星。
打印日志請使用LogUtil中的方法霞怀,因為框架對日志輸出進行了增強,統(tǒng)一添加了流水號(基于交換區(qū)對象)莉给。
LogUtil只能在當前主線程下使用毙石。
使用示例
- 定義業(yè)務接口協(xié)議:協(xié)議公共部分+協(xié)議業(yè)務部分
請求協(xié)議業(yè)務部分如下:parameter 對象
參數(shù) | 類型 | 是否必選 | 描述 |
---|---|---|---|
id | String | 是 | 業(yè)務主鍵 |
響應協(xié)議業(yè)務部分如下:data 對象
參數(shù) | 類型 | 描述 |
---|---|---|
id | Integer | 主鍵 |
data | String | 數(shù)據(jù) |
- 編寫實現(xiàn)代碼
獲取案例數(shù)據(jù)接口,帶參數(shù)Controller:ExampleController
/**
* 獲取案例數(shù)據(jù)接口,帶參數(shù)
* <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> {
/** 數(shù)據(jù)服務 */
@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("執(zhí)行業(yè)務方法");
return exampleService.getExampleInfo(p.getId()).get();
}
@Override
protected Class<BaseReq> getParamClass() {
return BaseReq.class;
}
}
獲取案例數(shù)據(jù)接口,無參數(shù)Controller:ExampleNonParamController
/**
* 獲取案例數(shù)據(jù)接口,無參數(shù)
* <p>說明:</p>
* <li></li>
* @author DuanYong
* @since 2017年7月17日上午9:02:56
*/
@RestController
@RequestMapping(value = "/example/v1/getNonParamExampleInfo")
public class ExampleNonParamController extends AbstractNonParamController {
/** 數(shù)據(jù)服務 */
@Autowired
private ExampleService exampleService;
@Override
public Object process() {
return exampleService.getExampleInfo("1");
}
}
獲取案例數(shù)據(jù)接口,url參數(shù)Controller:ExampleUrlParamController
/**
* 獲取案例數(shù)據(jù)接口,url參數(shù)
* <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 {
/** 數(shù)據(jù)服務 */
@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"));
}
}
請求業(yè)務對象:BaseReq
/**
* 查詢對象基類
* <p>說明:</p>
* <li>定義相關(guān)公共查詢字段</li>
* @author DuanYong
* @since 2017年7月17日上午8:55:10
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BaseReq extends BaseParameter {
/**
* ID
*/
private String id;
}
響應業(yè)務對象:ExampleDto
/**
* 參數(shù)
* <p>說明:</p>
* <li></li>
* @author DuanYong
* @since 2017年7月14日下午1:04:59
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExampleDto {
/**
* id
*/
private String id;
/**
* 數(shù)據(jù)
*/
private String data;
}
編寫:ExampleDao及ExampleDaoMapper.xml
/**
* Example服務DAO
* <p>說明:</p>
* <li></li>
* @author DuanYong
* @since 2017年7月14日下午1:37:04
*/
public interface ExampleDao {
/**
* 根據(jù)版塊ID ,查詢版塊內(nèi)容
* <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
/**
* 案例數(shù)據(jù)服務接口
* <p>說明:</p>
* <li>獲取詳細數(shù)據(jù)</li>
* @author DuanYong
* @since 2017年7月14日上午10:54:21
*/
public interface ExampleService {
/**
* 獲取版塊及版塊下內(nèi)容信息
* <p>說明:</p>
* <li></li>
* @author DuanYong
* @param id 參數(shù)
* @return
* @since 2017年7月14日上午11:23:21
*/
Optional<ExampleDto> getExampleInfo(String id);
}
實現(xiàn)服務:ExampleServiceImpl
/**
* 案例數(shù)據(jù)服務接口實現(xiàn)
* <p>說明:</p>
* <li></li>
* @author DuanYong
* @since 2017年7月14日下午1:30:18
*/
@Service
@Slf4j
public 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(); } /** * 校驗結(jié)果 * <li></li> * @author duanyong@jccfc.com * @date 2021/3/3 14:59 * @param result: 結(jié)果 * @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("請求返回業(yè)務json:{}",s); log.info("請求返回簽名:{}",baseResponse.getSign()); if (SignUtil.cloudVerifySign(baseResponse.getSign(), s,baseResponse.getTransactionSn(),baseResponse.getTimestamp().toString(), SECRET_KEY)) { log.info("返回數(shù)據(jù)合法"); } else { log.info("返回數(shù)據(jù)被篡改"); } }); } }
測試類:ExampleControllerTest
/** * 接口測試 * <li></li> * * @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())// 模擬發(fā)送post請求 .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))// 預期返回值的媒體類型text/plain;charset=UTF-8 .andReturn();// 返回執(zhí)行請求的結(jié)果 String result = mvcResult.getResponse().getContentAsString(); log.info("請求的結(jié)果:{}",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請求參數(shù):{"appKey":"CHENNEL_1","nonce":"20210309094818488EEE50B9E9061026","parameter":{"id":"lbGSlXJ0ZB7="},"parameterMap":{"id":"lbGSlXJ0ZB7="},"sign":"0defb4240c72d52e5b46803ce51a728f8650ccafa9b80f9dbd4e7cd716c0fa1d","timestamp":1615254498483,"transactionSn":"20210309094818490626096EFA956655"},原始URL請求參數(shù):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,參數(shù)對象:{id=lbGSlXJ0ZB7=},加解密字段:['id'] [ main] c.j.x.b.interceptor.HandlerInterceptor : 接口->getExampleInfo,預處理完成,耗時->0.489秒,流水號:20210309094818490626096EFA956655 [ main] c.j.x.e.controller.ExampleController : 執(zhí)行業(yè)務方法 [ 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,參數(shù)對象:{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 : 請求的結(jié)果:{"code":"200","data":{"data":"BOmRuJy1ta7=","id":"lbGSlXJ0ZB7="},"message":"請求成功","sign":"e8b4b758265a8a00d7628565d8fa17d7baeec7ebc4b3f01882b6ac56816e7d88","timestamp":1615254500416,"transactionSn":"20210309094818490626096EFA956655"} [ main] com.javacoo.xservice.example.BaseTest : 請求返回業(yè)務json:{"data":"BOmRuJy1ta7=","id":"lbGSlXJ0ZB7="} [ main] com.javacoo.xservice.example.BaseTest : 請求返回簽名:e8b4b758265a8a00d7628565d8fa17d7baeec7ebc4b3f01882b6ac56816e7d88 [ main] com.javacoo.xservice.example.BaseTest : 返回數(shù)據(jù)合法
-
配置及說明
profile = dev_envrimont spring.http.encoding.force=true #監(jiān)控 #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 ## 最小空閑連接數(shù)量 spring.datasource.hikari.minimum-idle=10 ## 空閑連接存活最大時間颓遏,默認600000(10分鐘) spring.datasource.hikari.idle-timeout=180000 ## 連接池最大連接數(shù)徐矩,默認是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 ## 數(shù)據(jù)庫連接超時時間,默認30秒滤灯,即30000 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.connection-test-query=SELECT 1 #==================應用相關(guān)配置============= #是否開啟請求限制:true->開啟,false->關(guān)閉 app.config.core.reqLimitEnabled=true #請求限制:每秒允許最大并發(fā)限制 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 #=======連接池========== # 連接池最大連接數(shù)(使用負值表示沒有限制),如果賦值為-1,則表示不限制曼玩;如果pool已經(jīng)分配了maxActive個jedis實例鳞骤,則此時pool的狀態(tài)為exhausted(耗盡) spring.redis.jedis.pool.max-active=200 # 連接池中的最大空閑連接,控制一個pool最多有多少個狀態(tài)為idle(空閑的)的jedis實例,默認值也是8 spring.redis.jedis.pool.max-idle=50 #========安全配置============ #簽名算法: #<li>1:請求參數(shù)串=請求報文體中 parameter 對象轉(zhuǎn)換為 json字符串(統(tǒng)一用fastjson)</li> #<li>2:將應用密鑰分別添加到 請求參數(shù)串+隨機數(shù)+時間戳 的頭部和尾部:secret+請求參數(shù)字符串+隨機數(shù)+時間戳+secret</li> #<li>3:對該字符串進行 SHA256 運算黍判,得到一個byte數(shù)組</li> #<li>4:將該byte數(shù)組轉(zhuǎn)換為十六進制的字符串豫尽,該字符串即是這些請求參數(shù)對應的簽名</li> #<li>5:HEX(SHA256(secret+請求參數(shù)字符串+隨機數(shù)+時間戳+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 #========接口業(yè)務數(shù)據(jù)加解密配置============ #加密算法:Base64(DES(value,secretKey)) #解密 #格式=> app.config.decode.decodeParamMap[(版本號_)接口名稱]=['解密參數(shù)1'],['解密參數(shù)2']... app.config.decode.decodeParamMap[getExampleInfo]=['id'] #加密 #格式=> encode.encodeParamMap[(版本號_)接口名稱]=['編碼參數(shù)名1'],['編碼參數(shù)名2']... app.config.encode.encodeParamMap[getExampleInfo]=['id'],['data'] #========接口加鎖配置============ #說明:參數(shù)為空時為方法級加鎖,否則是參數(shù)級加鎖 #格式=> app.config.lock.lockParamMap[(版本號_)接口名稱].secondTimeout=60</li> 必須 #格式=> app.config.lock.lockParamMap[(版本號_)接口名稱].params=['參數(shù)名1'],['參數(shù)名2']...</li> app.config.lock.lockParamMap[getExampleInfo].secondTimeout=10 app.config.lock.lockParamMap[getExampleInfo].params=['id'] #========日志配置============ app.config.log.impl=example #========授權(quán)配置============ app.config.auth.impl=example
插件開發(fā)指南
XService的插件機制:
基于xkernel 提供的SPI機制,結(jié)合SpringBoot注解 ConditionalOnBean,ConditionalOnProperty實現(xiàn)顷帖,
XService的擴展點:
- 授權(quán)服務: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
系統(tǒng)提供默認實現(xiàn):
xkernel spi 開發(fā)步驟及實現(xiàn)機制 見:https://gitee.com/javacoo/xkernel
XService插件開發(fā)示例:授權(quán)服務擴展
- 第一步實現(xiàn)授權(quán)服務接口:com.javacoo.xservice.example.service.impl.AuthServiceImpl
/**
* 授權(quán)服務實現(xiàn)
* <li></li>
*
* @author: duanyong@jccfc.com
* @since: 2021/3/3 13:40
*/
@Slf4j
public class AuthServiceImpl implements AuthService {
/**
* 授權(quán)
* <li></li>
*
* @param o : 參數(shù)
* @author duanyong@jccfc.com
* @date 2021/3/2 18:11
* @return: void true-> 成功
*/
@Override
public boolean auth(Object o) {
log.info("授權(quán):{}", o);
return true;
}
}
- 第二步編寫注冊文件:在工程類路徑下新建META-INF/services目錄,新建com.javacoo.xservice.base.support.auth.AuthService文件,內(nèi)容如下:
example=com.javacoo.xservice.example.service.impl.AuthServiceImpl
- 第三步修改配置文件
#========授權(quán)配置============
app.config.auth.impl=example
軟件架構(gòu)
快速開發(fā)框架基于SpringBoot2.X實現(xiàn)美旧。具體分為:終端展現(xiàn)層渤滞、網(wǎng)關(guān)層、應用層陈症、數(shù)據(jù)層蔼水。
- 終端展現(xiàn)層:終端分為電視端,微信端录肯,PC瀏覽器趴腋。
- 網(wǎng)關(guān)層:基于Kong,實現(xiàn)了服務注冊论咏,流量控制优炬,負載均衡,簽名驗簽等厅贪。
- 應用層:轉(zhuǎn)發(fā)展現(xiàn)層蠢护、遠程調(diào)用等對業(yè)務層的邏輯請求,由控制層完成請求的接入养涮、參數(shù)校驗葵硕、流轉(zhuǎn)調(diào)度。將所有請求接入后統(tǒng)一轉(zhuǎn)交給業(yè)務集成層完成具體的服務調(diào)用贯吓。
- 業(yè)務層:所有的業(yè)務邏輯代碼都集中在這一層懈凹,包括本地業(yè)務,和遠程服務悄谐。
- 數(shù)據(jù)層:提供訪問數(shù)據(jù)庫介评,緩存組件,統(tǒng)一了數(shù)據(jù)訪問接口爬舰。
框架設計
概述
- 框架的Web接入控制層基于SpringBoot實現(xiàn)们陆,充分利用SpringBoot提供的攔截器架構(gòu),對請求的接入和相關(guān)控制提供可撥插式的透明情屹、松耦合的服務坪仇。
框架提供了統(tǒng)一通用的Controller實現(xiàn)類BaseController。BaseController提供了統(tǒng)一的異常處理屁商,響應數(shù)據(jù)處理等烟很。 - 同時框架也對請求和響應數(shù)據(jù)提供了基類型,它們分別是:BaseRequest和BaseResponse蜡镶,并抽象了常用請求參數(shù)BaseParameter,統(tǒng)一了接口請求和響應的報文規(guī)范恤筛。
- 除基本的Controller和DataBean外官还,框架提供了JSON請求數(shù)據(jù)轉(zhuǎn)換攔截器、動態(tài)DataBean對象綁定攔截器毒坛、系統(tǒng)安全攔截器望伦、加解密林说,簽名驗簽等功能組件。具體內(nèi)容詳見后面描述屯伞。
- 插件體系設計:框架基于xkernel 提供的SPI機制腿箩,結(jié)合SpringBoot提供的 ConditionalOnBean,ConditionalOnProperty等注解,實現(xiàn)了靈活可擴展的插件體系劣摇。
框架Controller體系結(jié)構(gòu)
類結(jié)構(gòu)模型:帶參數(shù)
類結(jié)構(gòu)模型:不帶參數(shù)
- Controller中的handle方法珠移,接口處理入口方法,@RequestMapping注解末融,規(guī)定了整個業(yè)務處理流程:請求參數(shù)解析->檢查請求參數(shù)->業(yè)務處理->設置響應數(shù)據(jù)钧惧。
- Controller中的execute方法,執(zhí)行具體業(yè)務流程勾习,使用了熔斷機制浓瞪。
- Controller中的parse方法,解析請求參數(shù)巧婶,將請求參數(shù)中的業(yè)務參數(shù)對象轉(zhuǎn)換為服務使用的對象乾颁。
- Controller中的initBaseParameter,初始化初始請求參數(shù):比如獲取IP艺栈,MAC等信息英岭。
- Controller中的validate校驗請求中的業(yè)務參數(shù),由子類實現(xiàn)眼滤,如果參數(shù)檢查不通過巴席,請拋出參數(shù)異常:IllegalParameterException,由具體子類實現(xiàn)诅需。
- Controller中的process業(yè)務方法調(diào)用漾唉,由具體子類實現(xiàn)。
- Controller中的getParamClass獲取參數(shù)類型堰塌,由具體子類實現(xiàn)赵刑。
- Controller中的fallback 服務降級,默認返回REQUEST_TIMEOUT字符串场刑,框架統(tǒng)一處理拋出TimeoutException異常般此。
- Controller中的checkAuth授權(quán)校驗。
攔截器體系結(jié)構(gòu)
類結(jié)構(gòu)模型
- BaseInterceptor攔截器基類牵现,繼承自HandlerInterceptorAdapter铐懊,覆蓋了preHandle,postHandle,afterCompletion,afterConcurrentHandlingStarted方法,通過攔截器數(shù)組瞎疼,實現(xiàn)攔截器鏈效果科乎。
- HandlerInterceptor攔截器,繼承自BaseInterceptor贼急,基于HandlerStack處理器鏈組件實現(xiàn)了整個業(yè)務處理核心流程:
1.preHandle預處理流程包括:
a)初始化數(shù)據(jù)交換區(qū):基于ThreadLocal實現(xiàn)茅茂,封裝了此次請求的相關(guān)信息SwapAreaData捏萍,供整個請求過程中使用。
b)解析請求參數(shù):將原始請求參數(shù)轉(zhuǎn)換為框架內(nèi)部BaseRequest對象
c)執(zhí)行預處理流程:生成全局流水號空闲,依次執(zhí)行注冊的預處理器HandlerStack令杈,如參數(shù)解碼,參數(shù)校驗等碴倾。
2.postHandle提交處理流程包括:
a)依次執(zhí)行注冊的預處理器HandlerStack逗噩,如編碼,簽名等影斑。
b)設置響應數(shù)據(jù):響應數(shù)據(jù)轉(zhuǎn)換為目標格式(如JSON格式)
3.afterCompletion完成處理后續(xù)流程包括:
a)異步發(fā)布交易完成事件给赞。
b)釋放當前線程數(shù)據(jù)交換區(qū)數(shù)據(jù)
- LocaleInterceptor國際化信息設置(基于SESSION)攔截器,繼承自BaseInterceptor矫户,基于HandlerStack處理器鏈組件實現(xiàn)了整個業(yè)務處理核心流程:
1.preHandle預處理流程包括:
a)設置客戶端語言片迅。 - MaliciousRequestInterceptor惡意請求攔截器,繼承自BaseInterceptor皆辽,基于HandlerStack處理器鏈組件實現(xiàn)了整個業(yè)務處理核心流程:
1.preHandle預處理流程包括:
a)根據(jù)配置參數(shù)柑蛇,處理請求,攔截惡意請求驱闷。 - RequestLimitInterceptor請求流量限制攔截器耻台,繼承自BaseInterceptor,基于HandlerStack處理器鏈組件實現(xiàn)了整個業(yè)務處理核心流程:
1.preHandle預處理流程包括:
a)根據(jù)配置參數(shù)空另,限制處理請求數(shù)量盆耽。
異常體系結(jié)構(gòu)
類結(jié)構(gòu)模型
- BaseException框架異常基類扼菠,繼承自RuntimeException
- BusinessException業(yè)務類異常摄杂,繼承自BaseException
- DataParseException數(shù)據(jù)解析類異常,繼承自BaseException
- IllegalParameterException請求參數(shù)類異常循榆,繼承自BaseException
- RemoteException遠程接口調(diào)用類異常析恢,繼承自BaseException
- ServiceException服務內(nèi)部異常,繼承自BaseException
- TimeoutException請求超時異常秧饮,繼承自BaseException
過濾器體系結(jié)構(gòu)
類結(jié)構(gòu)模型
- CorsFilter跨域請求過濾器
- CsrfFilter跨站請求偽造攻擊過濾器
- XssFilter非法字符過濾器(防SQL注入映挂,防XSS漏洞)
授權(quán)組件結(jié)構(gòu)
類結(jié)構(gòu)模型
- auth校驗渠道是否已經(jīng)授權(quán)
事件體系結(jié)構(gòu)
類結(jié)構(gòu)模型
- TransCompleteEvent交易完成事件對象,基于Spring的事件機制盗尸。
- ApplicationContextProvider applicationContext提供者柑船。
- AsyncApplicationEventMulticaster 異步事件處理,為了實現(xiàn)異步事件處理泼各,這里需要重新實現(xiàn)SimpleApplicationEventMulticaste
處理器體系結(jié)構(gòu)
類結(jié)構(gòu)模型
- Handler處理器接口椎组。
- EnCodeHandler 編碼處理器。
- DeCodeHandler 解碼處理器历恐。
- ParamValidatorHandler參數(shù)校驗處理器寸癌。
- ResponseSignHandler返回數(shù)據(jù)簽名處理器。
- MethodLockHandler加鎖處理器弱贼。
- MethodUnLockHandler解鎖處理器蒸苇。
- HandlerStack處理器鏈組件接口。
- DefaultHandlerStack 默認處理器鏈實現(xiàn)
熔斷處理組件結(jié)構(gòu)
類結(jié)構(gòu)模型
- HystrixUtil Hystrix工具類吮旅。
- InvokeTimeoutNonParamHystrixCommand無參數(shù)調(diào)用超時Command溪烤。
- InvokeTimeoutParamHystrixCommand 帶參數(shù)調(diào)用超時Comman
數(shù)據(jù)交換區(qū)組件結(jié)構(gòu)
類結(jié)構(gòu)模型
- SwapArea 內(nèi)部數(shù)據(jù)交換區(qū)。
- SwapAreaHolder 內(nèi)部數(shù)據(jù)交換區(qū)Holder庇勃。
- SwapAreaManager數(shù)據(jù)交換區(qū)管理接口檬嘀。
- DefaultSwapArea默認數(shù)據(jù)交換區(qū)實現(xiàn)。
- DefaultSwapAreaManager 默認數(shù)據(jù)交換區(qū)管理器责嚷。
- ThreadLocalSwapAreaHolder 基于ThreadLoca實現(xiàn)數(shù)據(jù)交換區(qū)Holder鸳兽。
- SwapAreaUtils數(shù)據(jù)交換工具類。
統(tǒng)一異常處理組件結(jié)構(gòu)
類結(jié)構(gòu)模型
- exceptionHandler根據(jù)配置統(tǒng)一處理異常信息
表達式處理組件結(jié)構(gòu)
類結(jié)構(gòu)模型
- Expression parseExpression(String el) 根據(jù)指定的表達式獲取表達式對象
- Object getValue(String el, Object root) 根據(jù)指定的表達式從上下文中取值
- <T> T getValue(String el, Object root, Class<T> clazz) 根據(jù)指定的表達式和目標數(shù)據(jù)類型從上下文中取值
- void setValue(String el, Object value, Object root) 根據(jù)指定的表達式將值設置到上下文中
分布式鎖處理組件結(jié)構(gòu)
類結(jié)構(gòu)模型
- 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) 釋放鎖
服務日志處理組件結(jié)構(gòu)
類結(jié)構(gòu)模型
- default void record(SwapAreaData logData) 記錄日志
項目信息
路漫漫其修遠兮,吾將上下而求索
碼云:https://gitee.com/javacoo/xService
QQ群:164863067
作者/微信:javacoo
郵箱:xihuady@126.com