文章會先介紹下微服務中網關的作用,接着通過一個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依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
加入註解,通過@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包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
@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的依賴,如下所示
<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>
應用主類使用@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();
}
啓動該服務網關後,訪問:http://localhost:5555/api-a/add?a=1&b=2:返回401錯誤
http://localhost:5555/api-a/add?a=1&b=2&accessToken=token:正確路由到server-A,並返回計算內容
總結:1>網關通過路由功能屏蔽了服務的實現細節,實現了服務級別,負載均衡路由;
2>實現了接口權限校驗與微服務業務邏輯的解耦,通過網關中的過濾器的功能,在各個生命週期校驗請求內容,將原本的對外服務層前移,讓微服務更側重於自身的業務邏輯處理;
3>實現了斷路由,不會因爲具體微服務的故障而導致服務網管的阻塞;
參考文章:
http://blog.didispace.com/springcloud5/