在微服务架构中,后端服务每每不直接开放给调用端,而是经过一个公共网关根据请求的url,路由到相应的服务。 在网关中能够作一些服务调用的前置处理,好比权限验证。也能够经过动态路由,提供多个版本的api接口。
spring cloud 提供的技术栈中,使用netflix zuul来做为服务网关。前端
新建一个maven工程,修改pom.xml引入 spring cloud 依赖:java
<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 配置文件,在配置文件内容:git
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.javaspring
@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:docker
在浏览器中访问 http://localhost/add-service/add?a=1&b=2
,能够看到返回结果:shell
{ msg: "操做成功", result: 3, code: 200 }
屡次访问,查看 add-service-demo 的控制台输出,能够看到服务网关对请求分发作了负载均衡。apache
在对外提供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); }
这样服务端的接口就能够支持跨域访问了。api
自定义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
复制 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
应该能够看到刚才打包生成的镜像了。
在前一篇中,已经启动了 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 进行消息收发。