文章会先介绍下微服务中网关的作用,接着通过一个demo来演示下具体的微服务的一个注册与发现,网关路由的两种方式,最后总结下网关的重要性;
1.网关的引入
Spring Cloud微服务生态中,我们使用Spring Cloud Netflix中的Eureka实现了服务注册与发现;微服务之间通过Ribbon或Feign实现服务之间的调用以及负载均衡;通过Spring Cloud Config实现了应用多环境的外部化部署以及版本管理;为了使微服务集群更加健壮,使用hystrix的熔断机制避免某些服务出故障后引发的故障蔓延的情况;那么为什么要引入网关,如果不引入网关的微服务架构应该是下面的样子;
在这个架构中,Service A和Service B是内部服务,他们会注册到Eureka Service,Open Service是对外提供服务,通过负载均衡公开对外调度;那么这种架构的不足之处:破坏了服务的无状态性,如果需要对服务访问进行权限控制,那么开放的服务的权限控制会贯穿整个开发服务的业务逻辑;
为了解决上面这个问题,需要将权限控制的东西向上层抽取出来,最好的方式是有一个统一的网关对外提供服务,将权限控制放在网关处来处理;Spring Cloud Netflix中Zuul就担任了这样一种角色;下面通过一个demo来演示下zuul的具体的实现;
2.Demo
这里会有四个服务:一个注册中心eureka-server、两个简单的服务Service-A,Service-B,还有一个网关;通过eureka-server将服务A和B注册到服务中心;
1>eureka-server
引入eureka的jar依赖
org.springframework.cloud
spring-cloud-starter-eureka-server
加入注解,通过@EnableEurekaServer注解启动一个服务注册中心提供给其他应用进行对话,只需要在一个普通的Spring Boot应用中添加这个注解就能开启此功能
@EnableEurekaServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}
接下来是几个配置,在默认情况下,注册中心也会将自己作为客户端来注册它自己,需要禁用客户端注册行为
server.port=1111
#eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/
这样,注册中心eureka-server服务就好了,启动之后,可以通过访问http://localhost:1111/来监控当前注册中心中注册的一些服务信息,如图所示
可以看到现在还没有服务的实例注册进来;下面我们添加两个服务A和B
2>Service-A和Service-B
需要引入下面的jar包
org.springframework.cloud
spring-cloud-starter-eureka
@EnableDiscoveryClient
@SpringBootApplication
public class ComputeServiceApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ComputeServiceApplication.class).web(true).run(args);
}
}
标注了@EnableDiscoveryClient,该注解能激活Eureka中的DiscoveryClient实现,就可以注册到服务中心去;下面是配置文件,主要是服务的名称和配置中心的地址;
spring.application.name=service-A
server.port=2222
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
最后我们添加一个controller,实现两个数a和b相加的功能,并打印出当前服务的信息;
@RestController
public class ComputeController {
private final Logger logger = Logger.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@RequestMapping(value = "/add" ,method = RequestMethod.GET)
public String add(@RequestParam Integer a, @RequestParam Integer b) {
ServiceInstance instance = client.getLocalServiceInstance();
Integer r = a + b;
logger.info("/add, host:" + instance.getHost() + ", service_id:" + instance.getServiceId() + ", result:" + r);
return "From Service-A, Result is " + r;
}
}
启动之后,我们会发现注册中心,已经多了一个服务;并且该服务只有一个实例;
我们再以同样的方式添加服务B,只需要修改下端口和服务的名称,如下所示
spring.application.name=service-B
server.port=3333
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
然后启动服务B,服务中心会有两个服务了;
3>网关服务api-gateway
首先要引入依赖,主要是zuul和eureka的依赖,如下所示
org.springframework.cloud
spring-cloud-starter-zuul
org.springframework.cloud
spring-cloud-starter-eureka
应用主类使用@EnableZuulProxy注解开启Zuul
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
这里用了@SpringCloudApplication注解,通过源码我们看到,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker,主要目的还是简化配置,接着是配置文件的配置
spring.application.name=api-gateway
server.port=5555
定义了网关服务的端口和服务名称,然后是路由的配置,一般来说有两种方式,一种通过url映射,还有一种通过serveId的方式;
通过url映射方式的配置如下所示
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:2222/
这个配置定义了路由的规则:所有/api-a-url/**的访问映射到http://localhost:2222/上,也就说当访问http://localhost:5555/api-a-url/add?a=1&b=2的时候,Zuul会将该请求路由到:http://localhost:2222/add?a=1&b=2上
通过url映射的方式不太方便,因为需要我们的网关知道微服务的地址,但是其实是我们所有的服务已经注册到注册中心了,所以,最好的方式是通过服务名称去路由,所以另外一种方式就是通过服务id,我们只需要将zuul注册到eureka上让他去发现服务有哪些,就可以实现一个路由的功能;配置如下所示
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
这样通过/api-a/去访问的就会调用A服务,通过/api-b/去访问的会调用B服务;可以分别尝试下这两种访问方式
访问:http://localhost:5555/api-a-url/add?a=1&b=2:通过url映射的方式访问a服务,返回结果:From Service-A, Result is 3
http://localhost:5555/api-a/add?a=1&b=2:通过serviceId映射访问service-A中的add服务
http://localhost:5555/api-b/add?a=1&b=2:通过serviceId映射访问service-B中的add服务
通过serverId的方式除了对zuul使用更加友好,还支持断路由的功能,对于服务故障的情况下,可以有效的防止故障蔓延影响整个服务集群;
4>服务过滤功能
完成服务路由之后,对外开放的服务还需要一些安全措施来保护客户端对服务资源的访问,需要使用zuul的过滤器的功能来实现对外服务的安全控制;在服务网关中,定义过滤器只需要继承ZuulFilter抽象类并覆盖他的四个方法即可对请求进行拦截与过滤;下面通过一个例子做演示,会定义个过滤器来针对请求的参数中是否包含token做不同的处理,如果请求参数中不包含token,那么直接返回401;
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@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();
log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
Object accessToken = request.getParameter("accessToken");
if(accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
}
log.info("access token ok");
return null;
}
}
说明下这几个参数的具体含义:
filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
pre:可以在请求被路由之前调用
routing:在路由请求时候被调用
post:在routing和error过滤器之后被调用
error:处理请求时发生错误时被调用
filterOrder:通过int值来定义过滤器的执行顺序
shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。在上例中,我们直接返回true,所以该过滤器总是生效。
run:过滤器的具体逻辑。需要注意,这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。
定义了过滤器之后,我们还需要实例化过滤器,需要在主类中增加如下内容
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
启动该服务网关后,访问:总结:1>网关通过路由功能屏蔽了服务的实现细节,实现了服务级别,负载均衡路由;
2>实现了接口权限校验与微服务业务逻辑的解耦,通过网关中的过滤器的功能,在各个生命周期校验请求内容,将原本的对外服务层前移,让微服务更侧重于自身的业务逻辑处理;
3>实现了断路由,不会因为具体微服务的故障而导致服务网管的阻塞;
参考文章:
http://blog.didispace.com/springcloud5/