欢迎您使用 zq网关 ,下面是使用文档

#

zhangqi

2022/08/02

该项目已被git托管,你可以直接在gitee上拉取该项目。另外,该网页已被hoppin的网页 托管服务所托管,具体信息点我

下面简单介绍一下该项目,具体内容和细节可以访问git里的readme

起源

在逛开源社区的时候看到一篇博客 (现在已经没了), 内容是关于api中间件的架构思想。当时我觉得这个思想很独特,刚好作者留下了一个开源地址 , 并实现了部分内容,之后的内容就由我来补充和拓展。由于之前写了一套rpc框架, 加上这套网关中间件,急需一个项目来检验其可行性和健壮性, 就写了一个博客项目 并在那个项目里继续完善和添加功能并修补bug,就都提交在博客项目里了。 今天就把部分功能剥离出来,作为一个新的开源项目。

简介

本项目其实就是把我的剑来框架的网关解耦并剥离出来形成的一个独立项目, 可扩展性极高。由于我的剑来框架架构的独特性(没有Controller层),所以不支持swagger注解, 就手写了一个类似swagger的接口文档的功能并自定义了许多注解。在此基础上,zq网关诞生了。

以下是zq网关一个简单的流程图以及与普通请求流程的对比:

相信你已经迫不及待看demo了,你会发现@Controller和@Service注解已经没有了,取而代之的是网关服务注解@ApiServiceMapping和剑来RPC注解@ServiceRegister。这两个其实就是@Service注解,下面我会详细介绍。

网关引入

你有三种方式引入该网关

  1. 方式1、通过我的Maven私服Nexus引入

    ①、先引入仓库位置

    
    <repositories>
        <repository>
            <id>zhangqi</id>
            <name>zhangqi</name>
            <url>http://1.15.232.156:8081/nexus/content/groups/public/</url>
        </repository>
    </repositories>
    

    ②、再引入依赖

    
    <dependency>
        <groupId>com.hoppinzq</groupId>
        <artifactId>hoppinzq-service-gateway</artifactId>
        <version>1.1</version>
    </dependency>
    

  2. 方式2、直接引入jar包,点击下载hoppinzq-gateway-core-1.1.jar

  3. 方式3、(推荐)将其作为您项目的一个模块,这样你就可以定制你的网关并拓展。你只需要在gitee上拉取该项目,我帮你集成好了。

网关启动

你有两种方式启动网关,在这里我建议测试学习的时候用第一种,实际使用的时候用第二种

  1. 方式1、在启动类前加上zq网关开关注解 @EnableGateway ,这是最简单的方式。

  2. 方式2、如果你阅读源码你会发现,通过方法1启动的网关默认url规则是以hoppinzq为 后缀的请求,而且它的ApiGatewayHand处理器阉割看很多,可能无法满足您的要求,而你无意修改源码。这时你就可以用这个方法了: 你需要自己写一个Servlet。幸运的是,你并非无demo可循,你可以参照我的博客项目 是如何定制网关Servlet的

    新建一个类继承网关处理器ApiGatewayHand,然后在你的Servlet的init方法声明该处理器, 并重写doGet、doPost方法,然后指定url映射,最后将request,response使用你声明的处理器处理请求即可。

    新建的类重写你想要重写的方法以适应您的需求

    其中,尽量不要去重写的方法如下(这是因为ApiGatewayHand实现了InitializingBean, ApplicationContextAware两个接口):

  3. void setApplicationContext(ApplicationContext context);

    你不应该重写该方法!保持原处理器的该方法的完整!必须在该方法内将spring上下文ApplicationContext交给作为API注册中心的ApiStore

    void afterPropertiesSet();

    你不应该重写该方法!保持原处理器的该方法的完整!该方法是在ApiGatewayHand处理器被Spring正确实例化的时候调用的方法,该方法内必须调用ApiStore的loadApiFromSpringBeans方法来扫描api网关注解并存储

    以上两个方法你可以补充代码,但是不能删除原有的代码逻辑。可选择重写的方法如下:

    void handle(HttpServletRequest request, HttpServletResponse response)

    处理请求的核心代码,最好不要动原来的代码逻辑。

    void setOutParam(HttpServletRequest request,HttpServletResponse response,Object result)

    该方法会在返回数据时调用,其中result为逻辑层返回值或者异常。可以在该方法内自定义封装统一返回值或处理日志。

    Map<String,String> decodeParams(HttpServletRequest request,HttpServletResponse response) throws ResultReturnException

    该方法会在处理参数时调用,若请求中携带了encode参数。此时将对参数解码,返回解码后的参数列表。重写该方法以自定义解码规则。

    Boolean rightCheck(HttpServletRequest request,HttpServletResponse response,RequestParam requestParam) throws ResultReturnException

    权限校验,将在调用方法前调用,这个需要你去重写,因为每个人的权限校验都不尽相同。但是如果你想通过本网关提供的注解的形式,可参照博客项目的该方法的编写。返回true表示校验通过

    void type(HttpServletRequest request,HttpServletResponse response)

    类型校验,如果你在方法上添加了类型注解(只允许get或者post),将在该方法内校验。

    void sign(HttpServletRequest request,HttpServletResponse response)

    签名校验,若请求中携带了sign参数。

    void cache(HttpServletRequest request,HttpServletResponse response)

    缓存,重写该方法以实现自己的缓存机制。因为要解耦,就把redis删了,你可以借助blog博客的缓存设计。

    void token(HttpServletRequest request,HttpServletResponse response)

    检验token,实现接口幂等。

    void afterSuccessRequest(HttpServletRequest request,HttpServletResponse response)

    请求成功执行的操作,重写该方法以自定义操作。

    void afterErrorRequest(HttpServletRequest request,HttpServletResponse response)

    请求失败执行的操作,重写该方法以自定义操作。

    void afterRequest(HttpServletRequest request,HttpServletResponse response)

    请求完成后执行操作(无论成功与否),重写该方法执行自定义操作。

    以上所有方法内部都可以通过 (RequestParam)RequestContext.getPrincipal(); 获取本次请求的所有信息

    [收起/展开]

demo

引入网关后,就可以使用网关的注解了,这里介绍8个注解,其中ApiServiceMapping注解和ApiMapping注解是必须的

  1. ①、网关服务注解ApiServiceMapping

    /**
     * 自定义注解:注册服务类到Spring容器和服务类自定义信息
     */
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Service
    public @interface ApiServiceMapping {
        @AliasFor(
                annotation = Service.class
        )
        String value() default "";
        String title() default "";
        String description() default "";
    
        /**
         * 权限类型
         */
        ApiServiceMapping.RoleType roleType() default ApiServiceMapping.RoleType.NO_RIGHT;
    
        enum RoleType {
            NO_RIGHT,//服务类下的全部方法都不校验权限(此时通过为ApiMapping注解设置的权限会全部失效)
            RIGHT,//服务类下的全部方法都由方法自己决定是否校验权限(通过为ApiMapping注解设置RoleType的枚举值,方法自己默认不校验权限)
            ALL_RIGHT,//服务类下的全部方法都校验登录权限(此时通过为ApiMapping注解设置的权限会全部失效)
            ALL_ADMIN_RIGHT//服务类下的全部方法都校验管理员权限(此时通过为ApiMapping注解设置的权限会全部失效)
        }
    }
    

    需要加在任意(逻辑层)类上,如:

    @ApiServiceMapping(title = "服务标题", description = "服务描述",roleType = ApiServiceMapping.RoleType.RIGHT)
    public class DemoService {}
    

    其中title是服务标题,description是服务描述,roleType是权限枚举

  2. ①、网关方法注解ApiMapping 只会在注解ApiServiceMapping环绕的类内生效!

    /**
     * 自定义注解:要暴露的服务类方法
     * 只会在被ApiServiceMapping环绕的类中生效
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ApiMapping {
        String value();
        String title() default "";
        String description() default "";
        ApiMapping.Type type() default ApiMapping.Type.ALL;//请求类型
        enum Type {
            GET,//只允许GET请求
            POST,//只允许POST请求
            ALL,//所有请求都可
        }
        ApiMapping.RoleType roleType() default ApiMapping.RoleType.NO_RIGHT;//权限类型
        enum RoleType {
            NO_RIGHT,//不校验权限
            LOGIN,//校验登录权限
            MEMBER,//校验登录权限及会员权限,付费用户
            ADMIN,//超级管理员
        }
        /**
         * 声明返回值是否需要网关统一封装,封装规则需要配置,或者什么都不干用网关默认的{"code":xxx,"data":"xxx"}
         * 为true表示需要统一封装,这样前端就可以统一对this.response进行处理
         * 为false是由你去封装,直接返回方法return值。因为有一些前端组件已经定死了其解析规范,必须按照他的要求反参
         */
        boolean returnType() default true;
    
        /**
         * 是否加入接口日志
         */
        boolean log() default false;
    }
    

  3. 需要加在任意方法上,如:

    @ApiMapping(value = "test", title = "测试接口", description = "测试接口",roleType = ApiMapping.RoleType.LOGIN,returnType=false,type=ApiMapping.Type.POST,log = true)
    public String test(){
        //注册的接口列表,通过动态改变该列表的参数,实现对接口的定制和修改
        List<ServiceApiBean> serviceApiBeans= apiCache.outApiList;
        //本次请求的所有数据
        RequestParam requestParam=(RequestParam)RequestContext.getPrincipal();
        return "测试接口";
    }
    

    其中value是定义的请求映射名,为必填,在项目启动时,若发现有未赋值的value或重复的value就会不让启动。 title是方法标题,description是方法描述,roleType是权限枚举,上面那个表示只允许登录用户调用,未登录用户调用会抛出异常。returnType是是否封装返回值 为false表示不封装返回值。type是请求类型枚举,上面那个表示只允许POST请求调用。log是日志,为true表示(在返回值内)返回日志。

    进入网关的请求风格如下:

    
    http://127.0.0.1:端口号/网关后缀?method=方法名必须写&params={"参数名":"值"}&sign=签名可不写&token=可不写&encode=加密参数可不写&isEncodeReturn=可不写&timestamp=强制刷新可不写
                                        
    
    
    $.ajax({
        url:"http://127.0.0.1:端口号/网关后缀?method=方法名必须写",
        type:"post",
        data:JSON.stringify({
            "参数名":值
        }),
        dataType:"json"
    })
                                        
    
  4. ③、日志机制 @ApiMapping(value = "testLog", title = "测试接口-日志", description = "该接口会连日志一起返回",log = true)

     @ApiMapping(value = "testLog", title = "测试接口-日志", description = "该接口会连日志一起返回",log = true)
    public String testLog(){
        return "该接口是成功的,会连日志一起返回";
    }
    

  5. ④、权限机制@ApiMapping(value = "testLogin",roleType = ApiMapping.RoleType.LOGIN) 在项目启动后可以动态修改

    权限包括四种:未登录权限;登录权限;会员权限(付费用户);超级管理员权限。其中后两者默认有登录权限,没有权限的调用会抛出异常。该权限机制由你去实现,你可以参照博客项目的zauth鉴权

    /**
     * 权限配置
     * @return
     */
    @ApiMapping(value = "testLogin", title = "测试接口-权限", description = "必须登录用户可调用",roleType = ApiMapping.RoleType.LOGIN, log = true)
    public String testLogin(){
        return "该接口需要登录用户可调用";
    }
    

  6. ⑤、超时注解Timeout 在项目启动后无法修改

    /**
     * 自定义超时机制
     */
    @Target({ElementType.PARAMETER, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Timeout {
        int timeout() default 20000;
    }
    

  7. 需要加在任意方法上,如:

    /**
     * 超时机制
     * @return
     */
    @Timeout(timeout = 3000)
    @ApiMapping(value = "testTimeout", title = "测试接口-超时机制", description = "接口响应时间超过3秒报错测试", log = true)
    public String testTimeout(){
        try{
            Thread.sleep(4000);
        }catch(Exception exception){
    
        }
        return "接口响应时间超过3秒报错测试";
    }
    

  8. ⑥、返回值封装注解ReturnTypeUseDefault 只会在注解ApiMapping环绕的方法上生效!在项目启动后可以动态修改

    /**
     * 自定义注解:不封装返回值
     * 只会在被ApiServiceMapping环绕的类和被ApiMapping环绕的方法上生效
     * 优先级优于ApiMapping注解的returnType,也就是说如果方法有该注解,强行以该注解为主
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ReturnTypeUseDefault {
    }
    

  9. 需要加在任意方法上,如:

    /**
     * 返回值封装2
     * @return
     */
    @ReturnTypeUseDefault
    @ApiMapping(value = "testReturn2", title = "测试接口-返回值封装2", description = "该接口返回值不会被包装2")
    public String testReturn2(){
        return "该接口返回值不会被包装";
    }
    

    你还可以使用returnType=false,如:

    /**
     * 返回值封装
     * @return
     */
    @ApiMapping(value = "testReturn", title = "测试接口-返回值封装", description = "该接口返回值不会被包装",returnType=false)
    public String testReturn(){
        return "该接口返回值不会被包装";
    }
    

  10. ⑦、类型限定 @ApiMapping(value = "testPost", title = "测试接口-类型", description = "该接口只允许post请求",type=ApiMapping.Type.POST,log = true)

    /**
     * 请求类型
     * @return
     */
    @ApiMapping(value = "testPost", title = "测试接口-类型", description = "该接口只允许post请求",type=ApiMapping.Type.POST,log = true)
    public String testPost(){
        return "只允许post请求";
    }
    

  11. ⑧、缓存注解ApiCache 只会在注解ApiMapping环绕的方法上生效!在项目启动后可以动态修改

    /**
     * 自定义注解:缓存注解
     * 只会在被ApiServiceMapping环绕的类中和被ApiMapping环绕的方法上生效
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ApiCache {
        int time() default 60*5;//默认5分钟
    }
    

  12. 需要加在任意方法上,如:

    /**
     * 缓存机制
     * @return
     */
    @ApiCache
    @ApiMapping(value = "testCache", title = "测试接口-缓存", description = "该接口返回值会被缓存")
    public String testCache(){
        //注意,需要打开注释
        return "该接口返回值会被缓存,注意请打开ApiGatewayHand的注释,需要借助redis实现";
    }
    

  13. ⑨、幂等注解AutoIdempotent 只会在注解ApiMapping环绕的方法上生效!在项目启动后可以动态修改

    /**
     * 自定义注解:幂等注解,校验参数里的token
     * 只会在被ApiServiceMapping环绕的类中和被ApiMapping环绕的方法上生效
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface AutoIdempotent {
    }
    

  14. 需要加在任意方法上,如:

    /**
     * 幂等机制
     * @return
     */
    @AutoIdempotent
    @ApiMapping(value = "testAutoIdempotent", title = "测试接口-幂等", description = "该接口幂等")
    public String testAutoIdempotent(){
        return "该接口幂等,注意请打开ApiGatewayHand类里的token方法的注释,需要借助redis实现";
    }
    

  15. ⑩、限流注解ServiceLimit 优先级2,在项目启动后不能修改

    /**
     * 自定义注解限流
     */
    @Target({ElementType.PARAMETER, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Order(2)
    public  @interface ServiceLimit {
    	//描述
    	String description()  default "";
    	//key
    	String key() default "";
        //1s几次限制,小于等于0不会限制,最多每秒5次触发限制
        int number() default 5;
    	//类型
    	LimitType limitType() default LimitType.CUSTOMER;
    	enum LimitType {
    		//自定义key
    		CUSTOMER,
    		//根据请求者IP限流
    		IP
    	}
    }
    

  16. 需要加在任意方法上,如:

    /**
     * 限流机制
     * @return
     */
    @ServiceLimit(number = 1)
    @ApiMapping(value = "testLimit", title = "测试接口-限流", description = "该接口(同一ip)一秒只允许调用一次",log = true)
    public String testLimit(){
        return "该接口(同一ip)一秒只允许调用一次";
    }
    

  17. ⑩、同步锁注解Servicelock 优先级1,在项目启动后不能修改

    /**
     * 自定义注解同步锁
     */
    @Target({ElementType.PARAMETER, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Servicelock {
    	 String description()  default "";
    	//类型
    	Servicelock.LockType lockType() default LockType.DEFAULT;
    
    	enum LockType {
    		DEFAULT
    	}
    }
    

  18. 需要加在任意方法上,如:

     /**
     * 同步锁机制
     * @return
     * @throws Exception
     */
    @Servicelock
    @ApiMapping(value = "testLock", title = "测试接口-同步锁", description = "该接口被加入同步锁,共享数据只允许单线程操作")
    public String testLock() throws Exception{
        threadNum++;
        long l = new Double(10000*Math.random()).longValue();
        System.err.println("线程:"+threadNum+",进入,随机睡眠时间:"+l+" ms");
        if(ticket<=0){
            return "线程:"+threadNum+"买票,没有票了";
        }else{
            Thread.sleep(l);//买票操作,用随机时间模拟买票0~10秒
            ticket--;
            return "线程:"+threadNum+"买票,剩余票数:"+ticket;
        }
    }
    
    //没有加同步锁,会有负数票数的情况
    @ApiMapping(value = "testLock2", title = "测试接口-同步锁2", description = "该接口没有被加入同步锁")
    public String testLock2() throws Exception{
        threadNum2++;
        long l = new Double(10000*Math.random()).longValue();
        System.err.println("线程:"+threadNum2+",进入,随机睡眠时间:"+l+" ms");
        if(ticket2<=0){
            return "线程:"+threadNum2+"买票,没有票了";
        }else{
            Thread.sleep(l);//买票操作,用随机时间模拟买票0~10秒
            ticket2--;
            return "线程:"+threadNum2+"买票,剩余票数:"+ticket2;
        }
    }
    

动态修改

我们动态改变上面介绍的方法testReturn,先看一下方法testReturn的返回值是设置的不封装的:

先写一个接口,动态改变方法testReturn的内容:

@ApiMapping(value = "change", title = "测试接口-改变方法testReturn,使其封装返回值,并返回日志")
public void change(){
    List<ServiceApiBean> serviceApiBeans= apiCache.outApiList;
    List<ServiceMethodApiBean> serviceMethods=serviceApiBeans.get(0).getServiceMethods();
    for(ServiceMethodApiBean serviceMethodApiBean:serviceMethods){
        if("testReturn".equals(serviceMethodApiBean.getServiceMethod())){
            serviceMethodApiBean.setMethodReturn(true);
            serviceMethodApiBean.setLog(true);
        }
    }
}

再次调用testReturn,发现其实现细节已被修改:

zwagger接口可视化与配置

若你的项目集成了该网关,则内置了一个接口http://127.0.0.1:端口号/apiParams。

通过该接口的返回值,在页面回显,就成了自己的swagger了。你可以访问博客项目的zwagger

演示用博客项目接口[点击渲染]