具有负载均衡功能的RestTemplate底层原理(九)
在Spring Cloud服务管理框架Eureka简单示例(三)章节中,我们在服务调用端已经使用RestTemplate做了负载均衡,这里就详细解释一下RestTemplate底层原理,为什么一个Spring提供的做为Rest风格客户端的方法,在加了一个Ribbon提供的@LoadBalanced注解后,就能实现负载均衡了呢?这要得益于Ribbon的@LoadBalanced注解,它提供了一个拦截
解读
在Spring Cloud服务管理框架Eureka简单示例(三)章节中,我们在服务调用端已经使用RestTemplate做了负载均衡,这里就详细解释一下RestTemplate底层原理,为什么一个Spring提供的做为Rest风格客户端的方法,在加了一个Ribbon提供的@LoadBalanced注解后,就能实现负载均衡了呢?
这要得益于Ribbon的@LoadBalanced注解,它提供了一个拦截器,使得Spring在启动的时候,那些被@LoadBalanced注解修饰的RestTemplate类就不再是原来的那个RestTemplate类了,而是一个具有负载均衡功能的RestTemplate类。
自定义“负载均衡器”并实现拦截功能
我们新建一个简单java的maven项目rest-template,使用Spring Boot来搭建这个项目,所以先在pom.xml里面引入Spring Boot的依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.14.BUILD-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
在src/main/java下面创建org.init.springcloud包,之后创建我们的启动类MyApplication:
package org.init.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class,args);
}
}
参照使用了Ribbon @LoadBalanced注解的RestTemplate,我们去@LoadBalanced注解去编写一个自定义的负载均衡器注解MyLoadBalanced,去掉一些不相关的其他注解,保留主要的注解:
package org.init.springcloud;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
/**
* 用注解标记一个被配置用在负载均衡客户端上的RestTemplate Bean
* @author spirit
* @date 2018年5月15日 上午11:21:11
* @email spirit612@sina.cn
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MyLoadBalanced {}
接着编写一个Spring的配置类MyConfiguration,实现一个方法,收集我们自定义注解@MyLoadBalanced修饰的RestTemplate类,为了能够看到这种类的具体数目,我们再创建一个类,让它在初始化的时候,就能返回这个数目。
package org.init.springcloud;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class MyConfiguration {
@Autowired(required = false)
@MyLoadBalanced
private List<RestTemplate> tpls = Collections.emptyList();
//在初始化之后去创建一个实例bean
@Bean
public SmartInitializingSingleton loadBalanceInit(){
return () -> {
System.out.println(tpls.size());
};
}
}
这个时候运行MyApplication类的main方法,启动项目,我们就可以看到控制台打印出了数目:
这个时候当然是不可能有具体数目的,因为我们还没有用自定义注解修饰的RestTemplate。再新建一个控制器MyController,并且用自定义注解去修饰一个RestTemplate,让他在初始化的时候,就拥有我们自定义注解的功能:
package org.init.springcloud;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@Configuration
public class MyController {
@Bean
@MyLoadBalanced
public RestTemplate MyRestTemplate(){
return new RestTemplate();
}
}
再次启动项目,查看控制台输出:
被@MyLoadBalanced修饰的RestTemplate类还会被加上一些拦截器,这些拦截器是继承了ClientHttpRequestInterceptor接口(这个接口是Spring的内容,有困惑的读者可以自行查阅文档了解)的子类修饰的,Ribbon的底层就是用这些拦截器处理得到的请求,然后用自己特定的一些算法来实现负载均衡,我们这里也定义一个自己的拦截器类MyInterceptor,同样去实现ClientHttpRequestInterceptor接口:
package org.init.springcloud;
import java.io.IOException;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
public class MyInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execute) throws IOException {
System.out.println("这是我们自定义的拦截器");
System.out.println("请求的URI地址是:"+request.getURI());
return execute.execute(request, body);
}
}
然后是修改配置类MyConfiguration的loadBalanceInit方法,让RestTemplate类添加我们自定义的拦截器:
//在初始化之后去创建一个实例bean
@Bean
public SmartInitializingSingleton loadBalanceInit(){
return () -> {
System.out.println("被@MyLoadBalanced修饰的RestTemplate Bean的数目:"+tpls.size());
for (RestTemplate tpl : tpls) {
List<ClientHttpRequestInterceptor> interceptors = tpl.getInterceptors();
MyInterceptor myInterceptor = new MyInterceptor();
interceptors.add(myInterceptor);
}
};
}
为了不让RestTemplate以前的拦截器丢失,我们先获取了它原来所有的拦截器,之后再添加了一个自定义拦截器,紧接着就可以编写访问方法,测试我们添加的拦截器是否有效了。在控制器MyController里面添加两个方法,一个用于浏览器地址访问,一个用于RestTemplate内部请求:
@GetMapping(value = "/router")
@ResponseBody
public String router(){
RestTemplate temp = MyRestTemplate();
return temp.getForObject("http://localhost:8080/invoke", String.class);
}
@RequestMapping(method = RequestMethod.GET, value = "/invoke", produces = MediaType.APPLICATION_JSON_VALUE)
public String invoke(){
return "调用invoke方法";
}
启动项目,访问http://localhost:8080/router,我们可以看到浏览器返回了字符串,控制台也打印出了拦截器的信息:
可以看出我们的拦截器是生效了,接下来我们就简单模仿一下Ribbon的做法,实现一个低配版本的负载均衡,当然,我们不做那么复杂,仅仅是实现一个请求转发就OK了。新建一个MyHttpRequest类,实现HttpRequest接口,为这个类添加一个构造器,用于传入外部的Http请求,然后改造实现的三个方法,对headers和body部分都不做处理,我们只把这个请求的URI路径给改成一个自定义的地址:http://localhost:8080/forward
package org.init.springcloud;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
public class MyHttpRequest implements HttpRequest {
private HttpRequest httpRequest;
public MyHttpRequest(HttpRequest httpRequest){
this.httpRequest = httpRequest;
}
@Override
public HttpHeaders getHeaders() {
return httpRequest.getHeaders();
}
@Override
public HttpMethod getMethod() {
return httpRequest.getMethod();
}
@Override
public URI getURI() {
try {
URI myURI = new URI("http://localhost:8080/forward");
return myURI;
} catch (URISyntaxException e) {
e.printStackTrace();
}
return httpRequest.getURI();
}
}
然后在MyInterceptor拦截器里把请求给替换成我们的请求,也就是改写请求的URI
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execute) throws IOException {
System.out.println("这是我们自定义的拦截器");
System.out.println("请求的URI地址是:"+request.getURI());
HttpRequest myHttpRequest = new MyHttpRequest(request);
return execute.execute(myHttpRequest, body);
}
最后去MyController控制器里,添加一个方法,也就是我们前面路径里的方法:
@RequestMapping(method = RequestMethod.GET, value = "/forward", produces = MediaType.APPLICATION_JSON_VALUE)
public String forward(){
return "调用forward方法";
}
return "调用forward方法";
}
重启项目,访问http://localhost:8080/router,查看浏览器输出字符串和控制台输出:
我们可以看到,请求的确实是invoke方法,但是已经被转发到forward方法上了,Ribbon底层的做法和这个类似,只不过它拥有一套更完整的算法,来帮助我们处理服务请求的分发处理。
最后,大家有什么不懂的或者其他需要交流的内容,也可以进入我的QQ讨论群一起讨论:654331206
Spring Cloud系列:
Spring Cloud服务管理框架Eureka简单示例(三)
更多推荐
所有评论(0)