springboot+zuul实现自定义过滤器、动态路由、动态负载。

参考:https://blog.csdn.net/u014091123/article/details/75433656
https://blog.csdn.net/u013815546/article/details/68944039

Zuul是Netflix开源的微服务网关,他的核心是一系列的过滤器,通过这些过滤器我们可以轻松的实现服务的访问认证、限流、路由、负载、熔断等功能。

基于对已有项目代码零侵入的需求,本文没有将zuul网关项目注册到eureka中心,而是将zuul与springboot结合作为一个独立的项目进行请求转发,因此本项目是非spring cloud架构。

开始编写zuul网关项目
首先,新建一个spring boot项目。加入zuul依赖,开启@EnableZuulProxy注解。
pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
    <version>1.4.4.RELEASE</version>
</dependency>

application.properties

server.port=8090
eureka.client.enable=false
zuul.ribbon.eager-load.enabled=true

zuul.SendErrorFilter.post.disable=true

由于后续会使用到动态路由,所以这里我们并不需要在application.properties中做网关地址转发映射。

SpringBootZuulApplication.java

package com.syher.zuul;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.syher.zuul.core.zuul.router.PropertiesRouter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.ComponentScan;

import java.io.File;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @author braska
 * @date 2018/06/25.
 **/
@EnableAutoConfiguration
@EnableZuulProxy
@ComponentScan(basePackages = {
        "com.syher.zuul.core",
        "com.syher.zuul.service"
})
public class SpringBootZuulApplication implements CommandLineRunner {
    @Autowired
    ApplicationEventPublisher publisher;
    @Autowired
    RouteLocator routeLocator;

    private ScheduledExecutorService executor;
    private Long lastModified = 0L;
    private boolean instance = true;

    public static void main(String[] args) {
        SpringApplication.run(SpringBootZuulApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        executor = Executors.newSingleThreadScheduledExecutor(
                new ThreadFactoryBuilder().setNameFormat("properties read.").build()
        );
        executor.scheduleWithFixedDelay(() -> publish(), 0, 1, TimeUnit.SECONDS);
    }

    private void publish() {
        if (isPropertiesModified()) {
            publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
        }
    }

    private boolean isPropertiesModified() {
        File file = new File(this.getClass().getClassLoader().getResource(PropertiesRouter.PROPERTIES_FILE).getPath());
        if (instance) {
            instance = false;
            return false;
        }
        if (file.lastModified() > lastModified) {
            lastModified = file.lastModified();
            return true;
        }
        return false;
    }
}

一、自定义过滤器

自定义zuul过滤器比较简单。我们先讲过滤器。
zuul过滤器分为pre、route、post、error四种类型。作用我就不详细讲了,网上资料一大把。本文主要写路由前的过滤,即pre类型。
要自定义一个过滤器,只需要要继承ZuulFilter,然后指定过滤类型、过滤顺序、是否执行这个过滤器、过滤内容就OK了。

为了便于扩展,这里用到了适配器模式。
AbstractZuulFilter.java

package com.syher.zuul.core.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.syher.zuul.core.zuul.ContantValue;

/**
 * @author braska
 * @date 2018/06/29.
 **/
public abstract class AbstractZuulFilter extends ZuulFilter {

    protected RequestContext context;

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return (boolean) (ctx.getOrDefault(ContantValue.NEXT_FILTER, true));
    }

    @Override
    public Object run() {
        context = RequestContext.getCurrentContext();
        return doRun();
    }

    public abstract Object doRun();

    public Object fail(Integer code, String message) {
        context.set(ContantValue.NEXT_FILTER, false);
        context.setSendZuulResponse(false);
        context.getResponse().setContentType("text/html;charset=UTF-8");
        context.setResponseStatusCode(code);
        context.setResponseBody(String.format("{\"result\":\"%s!\"}", message));
        return null;
    }

    public Object success() {
        context.set(ContantValue.NEXT_FILTER, true);
        return null;
    }
}

定义preFilter的抽象类,继承AbstractZuulFilter。指定pre类型,之后所有的pre过滤器都可以继承这个抽象类。
AbstractPreZuulFilter.java

package com.syher.zuul.core.zuul.filter.pre;

import com.syher.zuul.core.zuul.FilterType;
import com.syher.zuul.core.zuul.filter.AbstractZuulFilter;

/**
 * @author braska
 * @date 2018/06/29.
 **/
public abstract class AbstractPreZuulFilter extends AbstractZuulFilter {
    @Override
    public String filterType() {
        return FilterType.pre.name();
    }
}

接着编写具体一个具体的过滤器,比如限流。
RateLimiterFilter.java

package com.syher.zuul.core.zuul.filter.pre;

import com.google.common.util.concurrent.RateLimiter;
import com.syher.zuul.core.zuul.FilterOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

/**
 * @author braska
 * @date 2018/06/29.
 **/
public class RateLimiterFilter extends AbstractPreZuulFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterFilter.class);

    /**
     * 每秒允许处理的量是50
     */
    RateLimiter rateLimiter = RateLimiter.create(50);

    @Override
    public int filterOrder() {
        return FilterOrder.RATE_LIMITER_ORDER;
    }

    @Override
    public Object doRun() {
        HttpServletRequest request = context.getRequest();
        String url = request.getRequestURI();
        if (rateLimiter.tryAcquire()) {
            return success();
        } else {
            LOGGER.info("rate limit:{}", url);
            return fail(401, String.format("rate limit:{}", url));
        }
    }
}

其他类型的过滤器也一样。创建不同的抽象类,比如AbstractPostZuulFilter,指定filterType,然后具体的postFilter只要继承该抽象类即可。

最后,将过滤器托管给spring。
ZuulConfigure.java

package com.syher.zuul.core.config;

import com.netflix.loadbalancer.IRule;
import com.netflix.zuul.ZuulFilter;
import com.syher.zuul.core.ribbon.ServerLoadBalancerRule;
import com.syher.zuul.core.zuul.filter.pre.RateLimiterFilter;
import com.syher.zuul.core.zuul.filter.pre.TokenAccessFilter;
import com.syher.zuul.core.zuul.filter.pre.UserRightFilter;
import com.syher.zuul.core.zuul.router.PropertiesRouter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author braska
 * @date 2018/07/05.
 **/
@Configuration
public class ZuulConfigure {

    @Autowired
    ZuulProperties zuulProperties;
    @Autowired
    ServerProperties server;

    /**
     * 动态路由
     * @return
     */
    @Bean
    public PropertiesRouter propertiesRouter() {
        return new PropertiesRouter(this.server.getServletPrefix(), this.zuulProperties);
    }

    /**
     * 动态负载
     * @return
     */
    @Bean
    public IRule loadBalance() {
        return new ServerLoadBalancerRule();
    }

    /**
     * 自定义过滤器
     * @return
     */
    @Bean
    public ZuulFilter rateLimiterFilter() {
        return new RateLimiterFilter();
    }
}

二、动态路由

接着写动态路由。动态路由需要配置可持久化且能动态刷新。
zuul默认使用的路由是SimpleRouteLocator,不具备动态刷新的效果。DiscoveryClientRouteLocator具备刷新功能,但是需要已有的项目将服务注册到eureka,这不符合已有项目代码零侵入的需求所以排除。那么还有个办法就是自定义路由然后实现RefreshableRouteLocator类。

部分代码如下:
AbstractDynamicRouter.java

package com.syher.zuul.core.zuul.router;

import com.syher.zuul.core.zuul.entity.BasicRoute;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @author braska
 * @date 2018/07/02.
 **/
public abstract class AbstractDynamicRouter extends SimpleRouteLocator implements RefreshableRouteLocator {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractDynamicRouter.class);

    public AbstractDynamicRouter(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
    }

    @Override
    public void refresh() {
        doRefresh();
    }

    @Override
    protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<String, ZuulProperties.ZuulRoute>();
        routes.putAll(super.locateRoutes());

        List<BasicRoute> results = readRoutes();

        for (BasicRoute result : results) {
            if (StringUtils.isEmpty(result.getPath()) ) {
                continue;
            }
            ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
            try {
                BeanUtils.copyProperties(result, zuulRoute);
            } catch (Exception e) {
                LOGGER.error("=============load zuul route info from db with error==============", e);
            }
            routes.put(zuulRoute.getPath(), zuulRoute);
        }
        return routes;
    }

    /**
     * 读取路由信息
     * @return
     */
    protected abstract List<BasicRoute> readRoutes();
}

由于本人比较懒。不想每次写个demo都要重新配置一大堆数据库信息。所以本文很多数据比如路由信息、比如负载策略。要么写在文本里面,要么直接java代码构造。
本demo的路由信息就是从properties里面读取。嗯,继承AbstractDynamicRouter即可。
PropertiesRouter.java

package com.syher.zuul.core.zuul.router;

import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.syher.zuul.common.Context;
import com.syher.zuul.core.zuul.entity.BasicRoute;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;

/**
 * @author braska
 * @date 2018/07/02.
 **/
public class PropertiesRouter extends AbstractDynamicRouter {

    private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesRouter.class);
    public static final String PROPERTIES_FILE = "router.properties";
    private static final String ZUUL_ROUTER_PREFIX = "zuul.routes";


    public PropertiesRouter(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
    }

    @Override
    protected List<BasicRoute> readRoutes() {
        List<BasicRoute> list = Lists.newArrayListWithExpectedSize(3);
        try {
            Properties prop = new Properties();
            prop.load(
                    this.getClass().getClassLoader().getResourceAsStream(PROPERTIES_FILE)
            );

            Context context = new Context(new HashMap<>((Map) prop));
            Map<String, String> data = context.getSubProperties(ZUUL_ROUTER_PREFIX);
            List<String> ids = data.keySet().stream().map(s -> s.substring(0, s.indexOf("."))).distinct().collect(Collectors.toList());
            ids.stream().forEach(id -> {
                Map<String, String> router = context.getSubProperties(String.join(".", ZUUL_ROUTER_PREFIX, id));

                String path = router.get("path");
                path = path.startsWith("/") ? path : "/" + path;

                String serviceId = router.getOrDefault("serviceId", null);
                String url = router.getOrDefault("url", null);

                BasicRoute basicRoute = new BasicRoute();
                basicRoute.setId(id);
                basicRoute.setPath(path);
                basicRoute.setUrl(router.getOrDefault("url", null));
                basicRoute.setServiceId((StringUtils.isBlank(url) && StringUtils.isBlank(serviceId)) ? id : serviceId);
                basicRoute.setRetryable(Boolean.parseBoolean(router.getOrDefault("retry-able", "false")));
                basicRoute.setStripPrefix(Boolean.parseBoolean(router.getOrDefault("strip-prefix", "false")));
                list.add(basicRoute);
            });
        } catch (IOException e) {
            LOGGER.info("error to read " + PROPERTIES_FILE + " :{}", e);
        }
        return list;
    }
}

既然是动态路由实时刷新,那肯定需要一个定时器定时监控properties文件。所以我在启动类SpringBootZuulApplication加了个定时器监控properties是否发生过变更(之前有疑问的现在可以解惑了)。一旦文件被修改过就重新发布一下, 然后会触发routeLocator的refresh方法。

public void publish() {
        if (isPropertiesModified()) {
            publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
        }
    }

当然,如果是从数据库或者其他地方比如redis读取就不需要用到定时器,只要在增删改的时候直接publish就好了。

最后,记得PropertiesRouter类交由spring托管(在ZuulConfigure类中配置bean)。

router.properties文件:

zuul.routes.dashboard.path=/**
zuul.routes.dashboard.strip-prefix=true

##不使用动态负载需指定url
##zuul.routes.dashboard.url=http://localhost:9000/
##zuul服务部署后,动态增加网关映射,无需重启即可实时路由到新的网关
##zuul.routes.baidu.path=/**

三、动态负载

负载也算比较简单,复杂点的是写负载算法。
动态负载主要分两个步骤:
1、根据网关项目配置的host和port去数据库(我是java直接造的数据)查找负载策略,比如轮询、比如随机、比如iphash等等。
2、根据策略结合每台服务器分配的权重选出合适的服务。

实现动态负载需要自定义rule类然后继承AbstractLoadBalancerRule类。
首先看负载策略的选择:
ServerLoadBalancerRule.java

package com.syher.zuul.core.ribbon;

import com.google.common.base.Preconditions;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.syher.zuul.common.util.SystemUtil;
import com.syher.zuul.core.ribbon.balancer.LoadBalancer;
import com.syher.zuul.core.ribbon.balancer.RandomLoadBalancer;
import com.syher.zuul.core.ribbon.balancer.RoundLoadBalancer;
import com.syher.zuul.entity.GatewayAddress;
import com.syher.zuul.service.GatewayService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

/**
 * @author braska
 * @date 2018/07/05.
 **/
public class ServerLoadBalancerRule extends AbstractLoadBalancerRule {

    private static final Logger LOGGER = LoggerFactory.getLogger(ServerLoadBalancerRule.class);

    @Value("${server.host:127.0.0.1}")
    private String host;
    @Value("${server.port:8080}")
    private Integer port;

    @Autowired
    private GatewayService gatewayService;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
    }

    @Override
    public Server choose(Object key) {
        return getServer(getLoadBalancer(), key);
    }

    private Server getServer(ILoadBalancer loadBalancer, Object key) {
        if (StringUtils.isBlank(host)) {
            host = SystemUtil.ipList().get(0);
        }
        //Preconditions.checkArgument(host != null, "server.host must be specify.");
        //Preconditions.checkArgument(port != null, "server.port must be specify.");

        GatewayAddress address = gatewayService.getByHostAndPort(host, port);
        if (address == null) { //这里的逻辑可以改,找不到网关配置信息可以指定默认的负载策略
            LOGGER.error(String.format("must be config a gateway info for the server[%s:%s].", host, String.valueOf(port)));
            return null;
        }

        LoadBalancer balancer = LoadBalancerFactory.build(address.getFkStrategyId());

        return balancer.chooseServer(loadBalancer);
    }

    static class LoadBalancerFactory {

        public static LoadBalancer build(String strategy) {
            GatewayAddress.StrategyType type = GatewayAddress.StrategyType.of(strategy);
            switch (type) {
                case ROUND:
                    return new RoundLoadBalancer();
                case RANDOM:
                    return new RandomLoadBalancer();
                default:
                    return null;
            }
        }
    }
}

然后是负载算法接口代码。
LoadBalancer.java

package com.syher.zuul.core.ribbon.balancer;

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

/**
 * @author braska
 * @date 2018/07/06.
 **/
public interface LoadBalancer {

    /**
     * choose a loadBalancer
     * @param loadBalancer
     * @return
     */
    Server chooseServer(ILoadBalancer loadBalancer);
}

定义抽象类,实现LoadBalancer接口
AbstractLoadBalancer.java

package com.syher.zuul.core.ribbon.balancer;

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.syher.zuul.core.SpringContext;
import com.syher.zuul.service.ServerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author braska
 * @date 2018/07/06.
 **/
public abstract class AbstractLoadBalancer implements LoadBalancer {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLoadBalancer.class);
    protected ServerService serverService;

    @Override
    public Server chooseServer(ILoadBalancer loadBalancer) {
        this.serverService = SpringContext.getBean(ServerService.class);
        Server server = choose(loadBalancer);
        if (server != null) {
            LOGGER.info(String.format("the server[%s:%s] has been select.", server.getHost(), server.getPort()));
        } else {
            LOGGER.error("could not find any server.");
        }
        return server;
    }

    public abstract Server choose(ILoadBalancer loadBalancer);
}

轮询负载算法
RoundLoadBalancer.java

package com.syher.zuul.core.ribbon.balancer;

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.syher.zuul.common.Constant;
import com.syher.zuul.core.GlobalCache;
import com.syher.zuul.core.ribbon.LoadBalancerRuleUtil;
import com.syher.zuul.entity.ServerAddress;

import java.util.List;

/**
 * 权重轮询
 * 首次使用取最大权重的服务器。而后通过权重的不断递减,寻找适合的服务器。
 * @author braska
 * @date 2018/07/06.
 **/
public class RoundLoadBalancer extends AbstractLoadBalancer {

    private Integer currentServer;
    private Integer currentWeight;
    private Integer maxWeight;
    private Integer gcdWeight;

    @Override
    public Server choose(ILoadBalancer loadBalancer) {
        List<ServerAddress> addressList = serverService.getAvailableServer();
        if (addressList != null && !addressList.isEmpty()) {
            maxWeight = LoadBalancerRuleUtil.getMaxWeightForServers(addressList);
            gcdWeight = LoadBalancerRuleUtil.getGCDForServers(addressList);
            currentServer = Integer.parseInt(GlobalCache.instance().getOrDefault(Constant.CURRENT_SERVER_KEY, -1).toString());
            currentWeight = Integer.parseInt(GlobalCache.instance().getOrDefault(Constant.CURRENT_WEIGHT_KEY, 0).toString());

            Integer serverCount = addressList.size();

            if (1 == serverCount) {
                return new Server(addressList.get(0).getHost(), addressList.get(0).getPort());
            } else {
                while (true) {
                    currentServer = (currentServer + 1) % serverCount;
                    if (currentServer == 0) {
                        currentWeight = currentWeight - gcdWeight;
                        if (currentWeight <= 0) {
                            currentWeight = maxWeight;
                            if (currentWeight == 0) {
                                GlobalCache.instance().put(Constant.CURRENT_SERVER_KEY, currentServer);
                                GlobalCache.instance().put(Constant.CURRENT_WEIGHT_KEY, currentWeight);
                                Thread.yield();
                                return null;
                            }
                        }
                    }

                    ServerAddress address = addressList.get(currentServer);
                    if (address.getWeight() >= currentWeight) {
                        GlobalCache.instance().put(Constant.CURRENT_SERVER_KEY, currentServer);
                        GlobalCache.instance().put(Constant.CURRENT_WEIGHT_KEY, currentWeight);
                        return new Server(address.getHost(), address.getPort());
                    }
                }
            }

        }
        return null;
    }
}

最后,ServerLoadBalancerRule交由spring托管。

至此,springboot+zuul实现自定义过滤器、动态路由、动态负载就都完成了。
源码:https://gitee.com/syher/spring-boot-project/tree/master/spring-boot-zuul

猜你喜欢

转载自blog.csdn.net/ragin/article/details/80944688