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://github.com/rxiu/study-on-road/tree/master/trickle-gateway

上一篇:JAVA中有一个特殊的类: Object


下一篇:Java 基于 mysql-connector-java 编写一个 JDBC 工具类