解读

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介绍与环境搭建(一)

Spring Boot的简单使用(二)

Spring Cloud服务管理框架Eureka简单示例(三)

Spring Cloud服务管理框架Eureka项目集群(四)

Spring Cloud之Eureka客户端健康检测(五)

Netflix之第一个Ribbon程序(六)

Ribbon负载均衡器详细介绍(七)

Spring Cloud中使用Ribbon(八)

具有负载均衡功能的RestTemplate底层原理(九)

OpenFeign之第一个Feign程序(十)

OpenFeign之feign使用简介(十一)

Spring Cloud中使用Feign(十二)

Netflix之第一个Hystrix程序(十三)

Netflix之Hystrix详细分析(十四)

Spring Cloud中使用Hystrix(十五)

Netflix之第一个Zuul程序(十六)

Spring Cloud集群中使用Zuul(十七)

Netflix之Zuul的进阶应用(十八)

消息驱动之背景概述(十九)

消息中间件之RabbitMQ入门讲解(二十)

消息中间件之Kafka入门讲解(二十一)

Spring Cloud整合RabbitMQ或Kafka消息驱动(二十二)

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐