在微服务的大趋势下,spring cloud相关技术越来越流行,但本人目前负责的业务项目中用spring cloud 的相关技术比较少,刚好组内有同学利用spring zull实现了自己的网关服务,因此借此机会学习学习。
网关致力于提供动态路由,监控,前置安全验证,断路等功能为一体的服务,为其他服务提供统一的外网调用接口。这里只重点介绍网关如何实现动态路由这一个关键点。
spring cloude 通用性架构。
1、外部或者内部的非Spring Cloud项目都统一通过API网关(Zuul)来访问内部服务.
2、网关接收到请求后,从注册中心(Eureka)获取可用服务
3、由Ribbon进行均衡负载后,分发到后端的具体实例
4、微服务之间通过Feign进行通信处理业务
5、Hystrix负责处理服务超时熔断
6、Turbine监控服务间的调用和熔断相关指标
image
没有网关的结构,Book*代表业务服务
image
book注册到eureka注册中心中,zuul本身也连接着同一个eureka,可以拉取book众多实例的列表。服务中心的注册发现一直是值得推崇的一种方式,但是不适用与网关产品。因为我们的网关是面向众多的其他部门的已有或是异构架构的系统,不应该强求其他系统都使用eureka,这样是有侵入性的设计。
image
上图中的gateway最终也会部署多台。properties文件中的路由一般是发布服务是硬编码固定的,容器启动时就一起加入了内存,另外需要加载DB中的路由信息,并且DB中的路由信息更新时需要及时刷新zull内存中的路由信息,那主要的关键点就是两个:1、容器启动时加载DB的路由信息,2、更新DB和zull中的路由信息。
要实现DB动态管理路由,首先简单实现properties方式的路由,了解其管理路由的方式。
gateway项目,即zull网关
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class,})
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
(exclude={DataSourceAutoConfiguration.class,}该注解的作用是,排除自动注入数据源的配置(取消数据库配置),否则启动时会报数据源等错误。
配置:
#配置在配置文件中的路由信息
zuul.routes.books.url=http://localhost:8090
zuul.routes.books.path=/book/**
# 不注册到euraka注册中心
ribbon.eureka.enabled=false
server.port=8080
logging.level.root=debug
book项目,即具体应用服务器
启动类&应用controller:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class BookApplication {
@RequestMapping(value = "/available")
public String available() {
System.out.println("Spring in Action");
return "Spring in Action";
}
@RequestMapping(value = "/checked-out")
public String checkedOut() {
return "Spring Boot in Action";
}
public static void main(String[] args) {
SpringApplication.run(BookApplication.class, args);
}
}
配置文件:
server.port=8090
启动两个项目在,访问http://localhost:8080/book/available,即能看到访问信息。
Spring in Action
上面就是一个简单的zull实现的简单静态路由,简单看一下部分源码是怎么实现的。
gateway启动类GatewayApplication.java中的注解,
@EnableZuulProxy ->ZuulProxyConfiguration -> ZuulConfiguration.java
/**
* @author Spencer Gibb
* @author Dave Syer
*/
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {
@Autowired
//zuul的配置文件,对应了application.properties中的配置信息
protected ZuulProperties zuulProperties;
@Autowired
protected ServerProperties server;
@Autowired(required = false)
//在项目中我们遇到404找不到的错误、或者500错误等,需要配置相应的页面给用户一个友好的提示时使用
private ErrorController errorController;
@Bean
public HasFeatures zuulFeature() {
return HasFeatures.namedFeature("Zuul (Simple)", ZuulConfiguration.class);
}
//核心类,路由定位器,最最重要
@Bean
@ConditionalOnMissingBean(RouteLocator.class)
public RouteLocator routeLocator() {
//系统配置的定位器SimpleRouteLocator.class
return new SimpleRouteLocator(this.server.getServletPrefix(),
this.zuulProperties);
}
//zuul的控制器,负责处理链路调用
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
//MVC HandlerMapping that maps incoming request paths to remote services.
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
mapping.setErrorController(this.errorController);
return mapping;
}
//注册了一个路由刷新监听器,路由更新后可以通过这个监听刷新路由
@Bean
public ApplicationListener zuulRefreshRoutesListener() {
return new ZuulRefreshListener();
}
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesn't
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
// pre filters
@Bean
public ServletDetectionFilter servletDetectionFilter() {
return new ServletDetectionFilter();
}
@Bean
public FormBodyWrapperFilter formBodyWrapperFilter() {
return new FormBodyWrapperFilter();
}
@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}
@Bean
public Servlet30WrapperFilter servlet30WrapperFilter() {
return new Servlet30WrapperFilter();
}
// post filters
@Bean
public SendResponseFilter sendResponseFilter() {
return new SendResponseFilter();
}
@Bean
public SendErrorFilter sendErrorFilter() {
return new SendErrorFilter();
}
@Bean
public SendForwardFilter sendForwardFilter() {
return new SendForwardFilter();
}
@Configuration
protected static class ZuulFilterConfiguration {
@Autowired
private Map filters;
@Bean
public ZuulFilterInitializer zuulFilterInitializer() {
return new ZuulFilterInitializer(this.filters);
}
}
//上面提到的路由刷新监听器
private static class ZuulRefreshListener
implements ApplicationListener {
@Autowired
private ZuulHandlerMapping zuulHandlerMapping;
private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent
|| event instanceof RefreshScopeRefreshedEvent
|| event instanceof RoutesRefreshedEvent) {
//设置为脏,下一次匹配到路径时,如果发现为脏,则会去刷新路由信息
this.zuulHandlerMapping.setDirty(true);
}
else if (event instanceof HeartbeatEvent) {
if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) {
this.zuulHandlerMapping.setDirty(true);
}
}
}
}
}
系统默认实现的定位器SimpleRouteLocator.class只是实现了RouteLocator.class中的方法,并没有提供刷新路由的方法,刷洗路由的方法在RefreshableRouteLocator.class中,但是能够借鉴DiscoveryClientRouteLocator.class定义自己的定位器。
RouteLocator.class接口:
public interface RouteLocator {
/**
* Ignored route paths (or patterns), if any.
*/
Collection getIgnoredPaths();
/**
* A map of route path (pattern) to location (e.g. service id or URL).
*/
List getRoutes();
/**
* Maps a path to an actual route with full metadata.
*/
Route getMatchingRoute(String path);
}
RefreshableRouteLocator.class接口:
public interface RefreshableRouteLocator extends RouteLocator {
void refresh();
}
SimpleRouteLocator.class具体内容:
@CommonsLog
public class SimpleRouteLocator implements RouteLocator {
private ZuulProperties properties;
private PathMatcher pathMatcher = new AntPathMatcher();
private String dispatcherServletPath = "/";
private String zuulServletPath;
private AtomicReference
重写之后的路由定位器:
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{
public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);
private JdbcTemplate jdbcTemplate;
private ZuulProperties properties;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
public CustomRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
this.properties = properties;
logger.info("servletPath:{}",servletPath);
}
@Override
public void refresh() {
doRefresh();
}
@Override
protected Map locateRoutes() {
LinkedHashMap routesMap = new LinkedHashMap();
//从application.properties中加载路由信息
routesMap.putAll(super.locateRoutes());
//从db中加载路由信息
routesMap.putAll(locateRoutesFromDB());
//优化一下配置
LinkedHashMap values = new LinkedHashMap<>();
for (Map.Entry entry : routesMap.entrySet()) {
String path = entry.getKey();
// Prepend with slash if not already present.
if (!path.startsWith("/")) {
path = "/" + path;
}
if (StringUtils.hasText(this.properties.getPrefix())) {
path = this.properties.getPrefix() + path;
if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, entry.getValue());
}
return values;
}
private Map locateRoutesFromDB(){
Map routes = new LinkedHashMap<>();
List results = jdbcTemplate.query("select * from gateway_api_define where enabled = true ",new BeanPropertyRowMapper<>(ZuulRouteVO.class));
for (ZuulRouteVO result : results) {
if(org.apache.commons.lang3.StringUtils.isBlank(result.getPath()) || org.apache.commons.lang3.StringUtils.isBlank(result.getUrl()) ){
continue;
}
ZuulRoute zuulRoute = new ZuulRoute();
try {
org.springframework.beans.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;
}
public static class ZuulRouteVO {
/**
* The ID of the route (the same as its map key by default).
*/
private String id;
/**
* The path (pattern) for the route, e.g. /foo/**.
*/
private String path;
/**
* The service ID (if any) to map to this route. You can specify a physical URL or
* a service, but not both.
*/
private String serviceId;
/**
* A full physical URL to map to the route. An alternative is to use a service ID
* and service discovery to find the physical address.eg.http://localhost:8090
*/
private String url;
/**
* Flag to determine whether the prefix for this route (the path, minus pattern
* patcher) should be stripped before forwarding.
*/
private boolean stripPrefix = true;
/**
* Flag to indicate that this route should be retryable (if supported). Generally
* retry requires a service ID and ribbon.
*/
private Boolean retryable;
private String apiName;
private Boolean enabled;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getServiceId() {
return serviceId;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public boolean isStripPrefix() {
return stripPrefix;
}
public void setStripPrefix(boolean stripPrefix) {
this.stripPrefix = stripPrefix;
}
public Boolean getRetryable() {
return retryable;
}
public void setRetryable(Boolean retryable) {
this.retryable = retryable;
}
public String getApiName() {
return apiName;
}
public void setApiName(String apiName) {
this.apiName = apiName;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
配置自定义的路由定位器:
@Configuration
public class CustomZuulConfig {
@Autowired
ZuulProperties zuulProperties;
@Autowired
ServerProperties server;
@Autowired
JdbcTemplate jdbcTemplate;
@Bean
public CustomRouteLocator routeLocator() {
CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServletPrefix(), this.zuulProperties);
routeLocator.setJdbcTemplate(jdbcTemplate);
return routeLocator;
}
}
最后只差动态刷新路由器,前面ZuulConfiguration.class中有对路由刷新做时间监控,因此,只需要更新路由后发送一个RoutesRefreshedEvent.class事件,zull就会自动接收监听进行路由刷新。因此简单增加一个路由刷新事件的service即可,
@Service
public class RefreshRouteService {
@Autowired
ApplicationEventPublisher publisher;
@Autowired
RouteLocator routeLocator;
public void refreshRoute() {
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routesRefreshedEvent);
}
}
再次启动两个项目,并且手动修改数据库中的路由信息,调用刷新接口后,能够访问到book服务,到此,动态路由的简单实现就已经完成。
这里有一张摘抄于SpringCloud官网的zuul的生命周期图片
image.png
小礼物走一走,来简书关注我
作者:黎明_dba5
链接:https://www.jianshu.com/p/a8f111c8f261
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。