前言
在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个公共网关根据请求的url,路由到相应的服务。 在网关中可以做一些服务调用的前置处理,比如权限验证。也可以通过动态路由,提供多个版本的api接口。
spring cloud 提供的技术栈中,使用netflix zuul来作为服务网关。
创建服务网关
新建一个maven工程,修改pom.xml引入 spring cloud 依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在 resources 目录中创建 application.yml 配置文件,在配置文件内容:
spring:
application:
name: @project.artifactId@
server:
port: 80
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8000/eureka/
zuul:
routes:
add-service-demo:
path: /add-service/**
serviceId: add-service-demo
这里主要关注 zuul 相关的配置,path 定义了需要路由的url,serviceId 和注册中心中的application 相对应,定义了路由到哪个服务。(这里 serviceId 应该换行和 path 同级,oschina 的markdown格式显示有问题)
在 java 目录中创建一个包 demo ,在包中创建启动入口 ServiceGatewayApplication.java
@EnableDiscoveryClient
@SpringBootApplication
@EnableZuulProxy
public class ServiceGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceGatewayApplication.class, args);
}
}
到这里一个简单的服务网关就配置好了,先启动注册中心 service-registry-demo,然后启动两个 add-service-demo 工程,分别映射到 8100 和 8101 端口,然后再启动刚刚配置好的服务网关 service-gateway-demo。
启动完成后访问注册中心页面 http://localhost:8000
,可以看到注册了两个 add-service-demo 和一个 service-gateway-demo:
在浏览器中访问 http://localhost/add-service/add?a=1&b=2
,可以看到返回结果:
{
msg: "操作成功",
result: 3,
code: 200
}
多次访问,查看 add-service-demo 的控制台输出,可以看到服务网关对请求分发做了负载均衡。
使用corsFilter解决前端跨域问题
在对外提供rest接口时,经常会遇到跨域问题,尤其是使用前后端分离架构时。
可以在服务端使用cors技术,解决前端的跨域问题。这里我们在网关层解决跨域问题。
修改 ServiceGatewayApplication.java ,增加一个CorsFilter,代码如下:
[@Bean](https://my.oschina.net/bean)
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
这样服务端的接口就可以支持跨域访问了。
使用自定义filter过滤请求
自定义filter也很简单,只需要继承 ZuulFilter 就可以了。
在demo包下新建一个filter的子包,用来存放自定义的filter类。新建一个filter类 MyFilter 继承 ZuulFilter:
@Component
public class MyFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String ip = getIpAddr(request);
System.out.println("收到来自IP为: '" + ip + "'的请求");
return null;
}
private String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
这个filter的逻辑很简单,就是从 HttpServletRequest 对象中获取请求IP,并打印出来。
然后在 ServiceGatewayApplication.java 中增加 MyFilter 的配置:
@Bean
public MyFilter myFilter() {
return new MyFilter();
}
重新启动 service-gateway-demo,再次发起请求,会看到控制台打印的IP。
我们回过头看一下,MyFilter 这个类从 ZuulFilter 继承之后做了哪些处理。
首先覆写 ZuulFilter 中的4个方法,分别为 filterType、filterOrder、 shouldFilter、 run。这4个方法看名字就知道有什么作用:
filterType
定义了filter的类型pre
表示在请求被路由之前调用route
请求被路由时调用,时机比pre
晚post
在路由完成后调用error
发生错误时调用
filterOrder
定义过滤器的执行顺序,值小的先执行shouldFilter
是否需要过滤run
过滤器的具体执行逻辑
demo源码 spring-cloud-1.0/service-gateway-demo
使用docker-maven-plugin打包并生成docker镜像
复制 application.yml,重命名为 application-docker.yml,修改 defaultZone为:
eureka:
client:
serviceUrl:
defaultZone: http://service-registry:8000/eureka/
这里修改了 defaultZone 的访问url,如何修改取决于部署docker容器时的 --link 参数, --link 可以让两个容器之间互相通信。
修改 application.yml 中的 spring 节点为:
spring:
application:
name: @project.artifactId@
profiles:
active: @activatedProperties@
这里增加了 profiles 的配置,在maven打包时选择不同的profile,加载不同的配置文件。
在pom.xml文件中增加:
<properties>
<java.version>1.8</java.version> <!-- 指定java版本 -->
<!-- 镜像前缀,推送镜像到远程库时需要,这里配置了一个阿里云的私有库 -->
<docker.image.prefix>
registry.cn-hangzhou.aliyuncs.com/ztecs
</docker.image.prefix>
<!-- docker镜像的tag -->
<docker.tag>demo</docker.tag>
<!-- 激活的profile -->
<activatedProperties></activatedProperties>
</properties>
<profiles>
<!-- docker环境 -->
<profile>
<id>docker</id>
<properties>
<activatedProperties>docker</activatedProperties>
<docker.tag>docker-demo-${project.version}</docker.tag>
</properties>
</profile>
</profiles>
<build>
<defaultGoal>install</defaultGoal>
<finalName>${project.artifactId}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!-- 配置spring boot maven插件,把项目打包成可运行的jar包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
<!-- 打包时跳过单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<!-- 配置docker maven插件,绑定install生命周期,在运行maven install时生成docker镜像 -->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.13</version>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>build</goal>
<goal>tag</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 修改这里的docker节点ip,需要打开docker节点的远程管理端口2375,
具体如何配置可以参照之前的docker安装和配置的文章 -->
<dockerHost>http://docker节点ip:2375</dockerHost>
<imageName>${docker.image.prefix}/${project.build.finalName}</imageName>
<baseImage>java</baseImage>
<!-- 这里的entryPoint定义了容器启动时的运行命令,容器启动时运行
java -jar 包名 , -Djava.security.egd这个配置解决tomcat8启动时,
因为需要收集环境噪声来生成安全随机数导致启动过慢的问题-->
<entryPoint>
["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/${project.build.finalName}.jar"]
</entryPoint>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<image>${docker.image.prefix}/${project.build.finalName}</image>
<newName>${docker.image.prefix}/${project.build.finalName}:${docker.tag}</newName>
<forceTags>true</forceTags>
<!-- 如果需要在生成镜像时推送到远程库,pushImage设为true -->
<pushImage>false</pushImage>
</configuration>
</plugin>
</plugins>
</build>
选择 docker
profile,运行 mvn install -P docker
,打包项目并生成docker镜像,注意docker-maven-plugin中的 <entryPoint>
标签里的内容不能换行,否则在生成docker镜像的时候会报错。
运行成功后,登录docker节点,运行 docker images
应该可以看到刚才打包生成的镜像了。
启动docker容器并注册服务
在前一篇中,已经启动了 service-registry-demo 和 add-service-demo,并且在两个容器之间建立了连接。这里启动 service-gateway-demo
docker run -d --name service-gateway-demo --publish 80:80 --link service-registry-demo:service-registry \
--link add-service-demo --volume /etc/localtime:/etc/localtime \
registry.cn-hangzhou.aliyuncs.com/ztecs/service-gateway-demo:docker-demo-1.0
这里比起上一篇启动 add-service-demo 容器的命令,有两个 --link ,分别连接了 service-registry-demo 和 add-service-demo,因为服务网关不仅需要注册到服务注册中心,还需要和后端提供的服务进行连接。
启动完成之后,访问注册中心的页面 http://宿主机IP:8000
查看服务注册信息,可以发现 service-gateway-demo 也注册成功了。
这时候就可以通过网关访问 add-service-demo 提供的服务了。
注意:在前一篇启动 add-service-demo 时使用了 --publish
把端口映射到了宿主机,在部署服务网关的情况下,后端服务就不需要映射到宿主机了,所有对服务的访问都通过网关进行路由,避免透过网关直接访问。
可以把3条启动命令封装到一个shell里:
docker run -d --name service-registry-demo --publish 8000:8000 \
--volume /etc/localtime:/etc/localtime \
registry.cn-hangzhou.aliyuncs.com/ztecs/service-registry-demo:docker-demo-1.0
echo 'sleep 30s to next step...'
sleep 30s
docker run -d --name add-service-demo --link service-registry-demo:service-registry \
--volume /etc/localtime:/etc/localtime \
registry.cn-hangzhou.aliyuncs.com/ztecs/add-service-demo:docker-demo-1.0
docker run -d --name service-gateway-demo --publish 80:80 --link service-registry-demo:service-registry \
--link add-service-demo --volume /etc/localtime:/etc/localtime \
registry.cn-hangzhou.aliyuncs.com/ztecs/service-gateway-demo:docker-demo-1.0
这里的 sleep 30s
是为了让 service-registry-demo 启动完成之后再启动 add-service-demo 和 service-gateway-demo。
在启动完成之后,通过网关访问接口时,可能会报错 Load balancer does not have available server for client: add-service-demo
这是因为 service-gateway-demo 和 add-service-demo 同时启动,service-gateway-demo 在向注册中心注册时,add-service-demo 可能还没有来得及注册,导致 service-gateway-demo 获取不到 add-service-demo 的注册信息,过个几十秒再访问就可以了。
最后
目前我们已经成功搭建了 服务注册中心、 服务网关、 后端服务,也创建了两个服务调用者 ribbon 和 feign。
配置中心 和 断路器 还没有涉及,配置中心 由于 spring cloud bus 需要用到消息队列 rabbitmq 或 kafka,在进行配置中心的开发之前,需要先部署消息队列。
断路器 的功能会和 hystrix-dashboard 断路监控一起放出,包括 turbine 、 zipkin、 spring cloud sleuth 服务调用追踪,这些都属于服务端异常监控范畴。
由于配置中心和服务追踪都涉及到消息队列,下一篇先脱离 spring cloud,介绍一下 docker 环境下的 rabbitmq 部署、AMQP 协议、以及使用 spring AMQP 进行消息收发。