xService微服务快速开发框架

XService:为组件化,快速构建微服务而生

什么是XService?

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>

添加完组件我们就可以进行配置使用了。

使用指南

XService的约定
  • 请求协议公共部分
参数类型是否必选描述
appKeyString应用key
nonceString32位UUID随机字串
signString签名
timestampLong请求时间戳,防止重放攻击
transactionSnString交易流水号
parameterObject请求的业务对象
  • 响应协议公共部分
参数类型描述
codeString返回码,具体含有参见下文
messageString返回消息,如错误信息
timestampString响应时间
transactionSnString交易流水号
signString签名
dataObject返回的业务对象
  • 返回码

平台级返回码如下:业务可制定具体的业务返回代码

codemessage说明
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
 */
@Slf4j
public 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
 */
@Slf4j
public 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 对象

参数类型是否必选描述
idString业务主键

响应协议业务部分如下:data 对象

参数类型描述
idInteger主键
dataString数据
  • 编写实现代码

    获取案例数据接口,带参数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
    @NoArgsConstructor
    public class BaseReq extends BaseParameter {
    	/**
    	 * ID
    	 */
    	private String id;
    }
    

    响应业务对象:ExampleDto

    /**
     * 参数
     * <p>说明:</p>
     * <li></li>
     * @author DuanYong
     * @since 2017年7月14日下午1:04:59
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class ExampleDto {
        /**
         * id
         */
        private String id;
    
        /**
         * 数据
         */
        private String data;
    }
    

    编写:ExampleDao及ExampleDaoMapper.xml

    /**
     * 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
    @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();
        }
        /**
         * 校验结果
         * <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
     */
    @Slf4j
    public class ExampleControllerTest extends BaseTest {
        private MockMvc mockMvc;
        @Autowired
        private WebApplicationContext wac;
        @Before
        public void setup() {
            this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
        }
    
        @Test
        public void getExampleInfoTest() throws Exception{
            MvcResult mvcResult = mockMvc.perform(post("/example/v1/getExampleInfo")
                .contentType(MediaType.APPLICATION_JSON)
                .content(FastJsonUtil.toJSONString(getExampleInfoReq())))
                .andExpect(status().isOk())// 模拟发送post请求
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))// 预期返回值的媒体类型text/plain;charset=UTF-8
                .andReturn();// 返回执行请求的结果
            String result = mvcResult.getResponse().getContentAsString();
            log.info("请求的结果:{}",result);
            verify(result);
        }
        private Object getExampleInfoReq() {
            BaseRequest baseRequest = new BaseRequest();
            baseRequest.setAppKey(APP_KEY);
            baseRequest.setTimestamp(Calendar.getInstance().getTimeInMillis());
            baseRequest.setNonce(getNonce());
            baseRequest.setTransactionSn(getNonce());
    
            BaseReq baseReq = new BaseReq();
            baseReq.setId(SecurityUtil.encryptDes("1",SECRET_KEY));
            String json = FastJsonUtil.toJSONString(baseReq);
            String sign = SignUtil.clientSign(json,baseRequest.getNonce(),baseRequest.getTimestamp().toString(),SECRET_KEY);
            baseRequest.setSign(sign);
            baseRequest.setParameter(baseReq);
            return baseRequest;
        }
    }
    
  • 测试日志

    [           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
    
    
    

插件开发指南

XService的插件机制:

基于xkernel 提供的SPI机制,结合SpringBoot注解 ConditionalOnBean,ConditionalOnProperty实现,

XService的扩展点:
  • 授权服务:com.javacoo.xservice.base.support.auth.AuthService
  • 服务日志记录服务:com.javacoo.xservice.base.support.log.LogService
  • 表达式解析:com.javacoo.xservice.base.support.expression.ExpressionParser
  • 分布式锁:com.javacoo.xservice.base.support.lock.Lock

系统提供默认实现:

ext.png

xkernel spi 开发步骤及实现机制 见:https://gitee.com/javacoo/xkernel

XService插件开发示例:授权服务扩展
  • 第一步实现授权服务接口:com.javacoo.xservice.example.service.impl.AuthServiceImpl
/**
 * 授权服务实现
 * <li></li>
 *
 * @author: duanyong@jccfc.com
 * @since: 2021/3/3 13:40
 */
@Slf4j
public 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实现。具体分为:终端展现层、网关层、应用层、数据层。

  1. 终端展现层:终端分为电视端,微信端,PC浏览器。
  2. 网关层:基于Kong,实现了服务注册,流量控制,负载均衡,签名验签等。
  3. 应用层:转发展现层、远程调用等对业务层的逻辑请求,由控制层完成请求的接入、参数校验、流转调度。将所有请求接入后统一转交给业务集成层完成具体的服务调用。
  4. 业务层:所有的业务逻辑代码都集中在这一层,包括本地业务,和远程服务。
  5. 数据层:提供访问数据库,缓存组件,统一了数据访问接口。
框架设计
概述
  • 框架的Web接入控制层基于SpringBoot实现,充分利用SpringBoot提供的拦截器架构,对请求的接入和相关控制提供可拨插式的透明、松耦合的服务。
    框架提供了统一通用的Controller实现类BaseController。BaseController提供了统一的异常处理,响应数据处理等。
  • 同时框架也对请求和响应数据提供了基类型,它们分别是:BaseRequest和BaseResponse,并抽象了常用请求参数BaseParameter,统一了接口请求和响应的报文规范。
  • 除基本的Controller和DataBean外,框架提供了JSON请求数据转换拦截器、动态DataBean对象绑定拦截器、系统安全拦截器、加解密,签名验签等功能组件。具体内容详见后面描述。
  • 插件体系设计:框架基于xkernel 提供的SPI机制,结合SpringBoot提供的 ConditionalOnBean,ConditionalOnProperty等注解,实现了灵活可扩展的插件体系。
框架Controller体系结构

类结构模型:带参数

带参数

类结构模型:不带参数

不带参数

  • 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 getValue(String el, Object root, Class 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
QQ群:164863067
作者/微信:javacoo
邮箱:xihuady@126.com
下载地址

https://gitee.com/javacoo/xService

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

javacoo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值