xService微服務快速開發(fā)框架

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):

ext.png

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)

總體邏輯架構(gòu)

快速開發(fā)框架基于SpringBoot2.X實現(xiàn)美旧。具體分為:終端展現(xiàn)層渤滞、網(wǎng)關(guān)層、應用層陈症、數(shù)據(jù)層蔼水。

  1. 終端展現(xiàn)層:終端分為電視端,微信端录肯,PC瀏覽器趴腋。
  2. 網(wǎng)關(guān)層:基于Kong,實現(xiàn)了服務注冊论咏,流量控制优炬,負載均衡,簽名驗簽等厅贪。
  3. 應用層:轉(zhuǎn)發(fā)展現(xiàn)層蠢护、遠程調(diào)用等對業(yè)務層的邏輯請求,由控制層完成請求的接入养涮、參數(shù)校驗葵硕、流轉(zhuǎn)調(diào)度。將所有請求接入后統(tǒng)一轉(zhuǎn)交給業(yè)務集成層完成具體的服務調(diào)用贯吓。
  4. 業(yè)務層:所有的業(yè)務邏輯代碼都集中在這一層懈凹,包括本地業(yè)務,和遠程服務悄谐。
  5. 數(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ù)

帶參數(shù)

類結(jié)構(gòu)模型:不帶參數(shù)

不帶參數(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)模型

類結(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)模型

AuthService.png
  • 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
下載地址

https://gitee.com/javacoo/xService

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末揍异,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子爆班,更是在濱河造成了極大的恐慌衷掷,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柿菩,死亡現(xiàn)場離奇詭異戚嗅,居然都是意外死亡,警方通過查閱死者的電腦和手機枢舶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門懦胞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人祟辟,你說我怎么就攤上這事医瘫。” “怎么了旧困?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵醇份,是天一觀的道長。 經(jīng)常有香客問我吼具,道長僚纷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任拗盒,我火速辦了婚禮怖竭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘陡蝇。我一直安慰自己痊臭,他們只是感情好哮肚,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著广匙,像睡著了一般允趟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸦致,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天潮剪,我揣著相機與錄音,去河邊找鬼分唾。 笑死抗碰,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的绽乔。 我是一名探鬼主播弧蝇,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼迄汛!你這毒婦竟也來了捍壤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤鞍爱,失蹤者是張志新(化名)和其女友劉穎鹃觉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體睹逃,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡盗扇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了沉填。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疗隶。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖翼闹,靈堂內(nèi)的尸體忽然破棺而出斑鼻,到底是詐尸還是另有隱情,我是刑警寧澤猎荠,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布坚弱,位于F島的核電站,受9級特大地震影響关摇,放射性物質(zhì)發(fā)生泄漏荒叶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一输虱、第九天 我趴在偏房一處隱蔽的房頂上張望些楣。 院中可真熱鬧,春花似錦、人聲如沸愁茁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽埋市。三九已至冠桃,卻和暖如春缩挑,著一層夾襖步出監(jiān)牢的瞬間舔痕,已是汗流浹背臼朗。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留污茵,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓葬项,卻偏偏與公主長得像泞当,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子民珍,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344