四、Spring Cloud-接口调用 Feign

2019-09-26   龙德   SpringCloud   Feign  

先看一下 SpringCloud 官网对它的定义

Feign 是一个声明式的 Web 服务客户端。它支持 Feign 本身的注解、JAX-RS 注解以及 SpringMVC 的注解。 SpringCloud 集成 Ribbon 和 Eureka 以在使用 Feign 时提供负载均衡的 http 客户端。

在 Spring Cloud 中使用 Feign

新建一个子工程,命名为 spring-cloud-consumer-feign

引入 Feign 依赖

<!-- 引入 Feign 依赖-->
<dependency>
  	<groupId>org.springframework.cloud</groupId>
  	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

application.properties

#指定服务名称
spring.application.name=spring-cloud-consumer-feign
#指定运行端口
server.port=8074
#获取注册实例列表
eureka.client.fetch-registry=true
#注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
#配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8080/eureka

启动类添加 @EnableFeignClients 注解,表明这是一个 Feign 客户端。

@EnableFeignClients
@EnableEurekaClient
@RestController
@SpringBootApplication
public class ConsumerFeignApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConsumerFeignApplication.class, args);
	}
}

新建一个 UserFeignClient 接口

@FeignClient(name = "spring-cloud-provider")
public interface UserFeignClient {

	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name);
	
}

注解 @FeignClient(name = “spring-cloud-provider”) 指定了调用哪个服务。

新建一个 UserController 类,并注入 UserFeignClient,就像调用本地方法一样调用远程接口。

@RestController
public class UserController {

	@Autowired
	private UserFeignClient userFeignClient;
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name) {
		return userFeignClient.getUser(name);
	}
}

启动 spring-cloud-eureka-server、spring-cloud-provider 和 spring-cloud-consumer-feign,访问 http://localhost:8074/users/zhangsan 同样能取得数据。

继承特性

从上面的例子可以看到,UserFeignClient 和 UserController 其实是声明与实现的关系,Feign 是通过接口的形式调用的,利用 Feign 的继承特性,可以把服务的接口单独抽出来,作为公共的依赖,以方便使用。

定义公共的 API 接口

新建一个子工程,命名为 spring-cloud-feign-api

因为要用到 MVC 的注解,所以引入 Feign 依赖,当然引入 web 依赖也可以。

<!-- 引入 Feign 依赖-->
<dependency>
  	<groupId>org.springframework.cloud</groupId>
  	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

定义一个接口,我这里以 api 开头,以便区分。

public interface UserApiService {
	
	@GetMapping("/api/users/{name}")
	public String getApiUser(@PathVariable("name") String name);
}

在实际项目中,这个工程应该要打成 jar 包的形式供其他工程使用的,这里我就不成 jar 包了,直接引用。

服务提供者实现接口

spring-cloud-provider 工程引入 spring-cloud-feign-api

<dependency>
	<groupId>com.example</groupId>
	<artifactId>spring-cloud-feign-api</artifactId>
	<version>0.0.1-SNAPSHOT</version>
</dependency>

新建一个 Controller,实现 UserApiService 接口

@RestController
public class UserApiController implements UserApiService {

	@Override
	public String getUserApi(String name) {
		return name + ", from user api";
	}

}

不同的是这里不需要在方法上面添加 @GetMapping 注解,这些注解在父接口中都有,不过在 Controller 上还是要添加 @RestController 注解。

服务消费者继承接口

spring-cloud-consumer-feign 同样引入 spring-cloud-feign-api

然后 UserFeignClient 继承 UserApiService

@FeignClient(name = "spring-cloud-provider")
public interface UserFeignClient extends UserApiService {
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name);
	
}

controller

@RestController
public class UserController {

	@Autowired
	private UserFeignClient userFeignClient;
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name) {
		return userFeignClient.getUser(name);
	}
	
	@GetMapping("/api/users/{name}")
	public String getApiUser(@PathVariable("name") String name) {
		return userFeignClient.getUserApi(name);
	}
}

访问结果

image

修改 Feign 的默认配置

修改 Feign 的默认配置也存在包扫描的问题,跟修改 Ribbon 的策略一样,我们使用注解的方式忽略扫描的类。

新建一个 config 包,新建类 FeignContract。

@ExcludeFromComponentScan
@Configuration
public class FeignContractConfig {

	@Bean
	public Contract feignContract() {
		return new Contract.Default();
	}
}

在 UserFeignClient 类中的注解 @FeignClient 指定 configuration 参数。

@FeignClient(name = "spring-cloud-provider", configuration = FeignContractConfig.class)

启动类指定包扫描忽略使用 @ExcludeFromComponentScan 注解的类

//忽略使用  @ExcludeFromComponentScan 注解的类
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = ExcludeFromComponentScan.class)})
@EnableFeignClients
@EnableEurekaClient
@RestController
@SpringBootApplication
public class ConsumerFeignApplication {

我们在 FeignContractConfig 类中修改了 Feign 的 Contract ,Contract 是一个契约的概念。 Feign 默认的契约是 SpringMVC,所以我们在 UserFeignClient 类中使用的是 SpringMVC 的注解。现在 Contract.Default() 使用的契约是 Feign 自己的,也就是说我们要把 SpringMVC 的注解修改为 Feign 的注解,否则项目启动不了。

@FeignClient(name = "spring-cloud-provider", configuration = FeignContractConfig.class)
public interface UserFeignClient extends UserApiService {
	
	// SpringMVC 版本
	// @GetMapping("/users/{name}")
	// public String getUser(@PathVariable("name") String name);
	
	// Feign 版本
	@RequestLine("GET /users/{name}")
	public String getUser(@Param ("name") String name);
	
}

源码下载:https://github.com/miansen/SpringCloud-Learn

三、Spring Cloud-负载均衡 Ribbon

2019-09-25   龙德   SpringCloud   Ribbon  

Ribbon 简介

目前主流的负载方案分为以下两种:

  • 服务端负载均衡(Nginx)

服务实例的清单在服务端,服务器进行负载均衡算法分配

  • 客户端负载均衡(Ribbon)

服务实例的清单在客户端,客户端进行负载均衡算法分配,Ribbon 就属于客户端自己做负载。

单独使用 Ribbon

我们使用 Ribbon 来实现一个最简单的负载均衡调用功能,接口就用 spring-cloud-provider 工程提供的 /users/{name} 接口,不过需要改动一下 controller 层

@RestController
public class UserController {

	@Value("${spring.application.name}")
	private String applicationName;
	
	@Value("${server.port}")
	private String post;
	
	@GetMapping("/users/{name}")
	public String users(@PathVariable("name") String name) {
		return String.format("hello %s,from server %s,post: %s", name, applicationName, post);
	}
}

我们把服务名和端口也返回给客户端,目的是能更直观的看到负载均衡的效果。

因为要测试负载均衡,所有需要启动多个服务。这里我启动两个 spring-cloud-provider 服务,端口分别是 8071 和 8072。

接着新建一个 Maven 项目,命名为 ribbon-demo

pom.xml 引入以下依赖

<dependency>
	<groupId>com.netflix.ribbon</groupId>
	<artifactId>ribbon</artifactId>
	<version>2.2.2</version>
</dependency>
<dependency>
	<groupId>com.netflix.ribbon</groupId>
	<artifactId>ribbon-core</artifactId>
	<version>2.2.2</version>
</dependency>
<dependency>
	<groupId>com.netflix.ribbon</groupId>
	<artifactId>ribbon-loadbalancer</artifactId>
	<version>2.2.2</version>
</dependency>

随便新建一个测试类,在 main 方法里调用接口

public static void main(String[] args) {
		// 服务列表
		List<Server> serverList = Lists.newArrayList(new Server("localhost", 8071), new Server("localhost", 8072));

		// 构建负载均衡器的实例
		BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
		for (int i = 0; i < 5; i++) {
			// 构建执行负载均衡器命令的实例,用于中转请求到负载均衡器
			LoadBalancerCommand<String> loadBalancerCommand = LoadBalancerCommand.<String>builder()
					.withLoadBalancer(loadBalancer).build();
			Observable<String> observable = loadBalancerCommand.submit(new ServerOperation<String>() {

				@Override
				public Observable<String> call(Server server) {
					try {
						String addr = String.format("http://%s/users/zhangsan", server.getHostPort());
						System.out.println("调用地址:" + addr);
						URL url = new URL(addr);
						HttpURLConnection conn = (HttpURLConnection) url.openConnection();
						conn.setRequestMethod("GET");
						conn.connect();
						InputStream in = conn.getInputStream();
						byte[] data = new byte[in.available()];
						in.read(data);
						return Observable.just(new String(data));
					} catch (Exception e) {
						return Observable.error(e);
					}
				}
			});
			String result = observable.toBlocking().first();
			System.out.println("调用结果:" + result);
		}
	}

控制台输出如下

image

从输出的结果中可以看到,负载均衡起作用了,8071 调用了 2 次,8072 调用了 3 次。

在 Spring Cloud 中使用 Ribbon

Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。 Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API 网关的请求转发等内容,实际上都是通过 Ribbon 来实现的。

在 Spring Cloud 中使用 Ribbon 也挺简单,因为 Spring Cloud 已经帮我们封装好了。甚至都不需要引入依赖,因为 Eureka 中已经引用了 Ribbon。

还记得我们之前在 spring-cloud-consumer 工程里注入了 RestTemplate 吗?

@Configuration
public class BeanConfiguration {

	@Bean
	@LoadBalanced
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
}

其实这就已经实现了负载均衡的效果了,RestTemplate 加了一个 @LoadBalanced 注解之后,就可以结合 Eureka 来动态发现服务并进行负载均衡的调用。

我们再启动服务注册中心 spring-cloud-eureka-server 和服务消费者 spring-cloud-consumer 验证一下。

启动成功后,访问服务消费者 http://localhost:8073/users/zhangsan,每次访问都会轮流的调用不同端口的消费提供者(注意看端口的变化)。

image

自定义负载均衡策略

上面的例子采用的是默认的负载均衡策略,也就是轮训的方式。我们也可以自定义负载均衡的策略。

自定义负载均衡策略有两种方式

  • 第一种方式

通过实现 IRule 接口自定义负载均衡策略,主要的选择服务逻辑在 choose 方法中。因为我们这里只是演示怎么自定义负载策略,所以没写选择的逻辑,而是直接返回服务列表中第一个服务。具体代码如下所示:

public class MyRule implements IRule {

	private ILoadBalancer lb;

	@Override
	public Server choose(Object key) {
		List<Server> servers = lb.getAllServers();
		return servers.get(0);
	}

	@Override
	public void setLoadBalancer(ILoadBalancer lb) {
		this.lb = lb;
	}

	@Override
	public ILoadBalancer getLoadBalancer() {
		return lb;
	}

}

在 Spring Cloud 中,可通过配置的方式使用自定义的负载均衡策略。在 application.propertise 里加入以下配置:

# 自定义的负载策略
spring-cloud-provider.ribbon.NFLoadBalancerRuleClassName=org.spring.cloud.consumer.config.MyRule

注意 spring-cloud-provider 是调用的服务名称。

也可以通过注解的方式使用自定义的负载均衡策略。

启动类上新增 @RibbonClient 注解

@RibbonClient(name = "spring-cloud-provider", configuration = MyRule.class)

这里的 spring-cloud-provider 同样也是调用的服务名称。

  • 第二种方式

通过继承 AbstractLoadBalancerRule 抽象类实现负载均衡的策略,主要的选择服务逻辑也是在 choose 方法中。具体的代码如下所示:

public class MyRule extends AbstractLoadBalancerRule {

	@Override
	public Server choose(Object key) {
		List<Server> servers = getLoadBalancer().getAllServers();
		return servers.get(0);
	}

	@Override
	public void initWithNiwsConfig(IClientConfig clientConfig) {
		// TODO Auto-generated method stub
		
	}
}

同样的,可以通过配置的方式或者是注解的方式使用自定义的负载均衡策略。

重启后再次访问服务消费者 http://localhost:8073/users/zhangsan,这时候端口应该不会发生变化,因为我们每次访问的都是服务列表中的第一个服务。

源码下载:https://github.com/miansen/SpringCloud-Learn

二、Spring Cloud-服务注册中心 Eureka

2019-09-24   龙德   SpringCloud   SpringCloud  

在上一篇 SpringCloud-服务提供者与服务消费者 中,服务提供者和服务消费者其实是两个独立的应用,并且服务调用的地址是硬编码在代码中的。我们需要一个注册中心(Eureka)来调度各个服务,并且监控各个服务的健康状态。

Spring Cloud Eureka 是 Spring Cloud Netflix 微服务套件的一部分,基于 Netflix Eureka 做了二次封装,主要负责实现微服务架构中的服务治理功能。

创建服务注册中心

新建一个子工程 spring-cloud-eureka-server,作为服务注册中心。

pom.xml

<?xml version="1.0"?>
<project
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
	<modelVersion>4.0.0</modelVersion>

	<parent>
		<groupId>com.example</groupId>
		<artifactId>spring-cloud-pom</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>

	<artifactId>spring-cloud-eureka-server</artifactId>
	<name>spring-cloud-eureka-server</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<!-- eureka server -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
		</dependency>
	</dependencies>
</project>

application.properties

# 服务名称
spring.application.name=spring-cloud-eureka-server
# 运行端口
server.port=8070
# 是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch-registry=false
# 是否要注册到注册中心(注册中心不需要开启)
eureka.client.register-with-eureka=false
# 关闭保护模式
eureka.server.enable-self-preservation=false

启动类

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(EurekaServerApplication.class, args);
	}
}

启动类多了一个 @EnableEurekaServer 注解,表示开启 Eureka Server。

启动工程,访问 http://localhost:8070,便会看到 Eureka 提供的 Web 控制台。

界面如下:

image

因为没有注册服务所以提示 “No instances available”。

创建服务提供者

改造原先的 spring-cloud-provider 工程

pom.xml 添加 Eureka Client 依赖

<!-- 引入 eureka client 依赖-->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改 application.properties 文件,添加以下配置:

# 指定服务名称
spring.application.name=spring-cloud-provider
# 指定运行端口
server.port=8071
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka

启动类

@EnableEurekaClient
@SpringBootApplication
public class ProviderApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProviderApplication.class, args);
	}
	
}

启动类多了一个 @EnableEurekaClient 注解,表示将此服务注册到 Eureka Server。

接着启动 spring-cloud-provider 工程,刷新 http://localhost:8070,可以看到 spring-cloud-provider 服务注册到 Eureka 了。

image

一个服务可以有多个实例,我们修改 spring-cloud-provider 工程的 application.properties 文件,只需要修改端口号就行了。

比如我修改成 8072,然后再启动。(如果你是用 IDEA,需要点击 Edit Configuration,将默认的 Single instance only (单实例) 的钩去掉才能启动)

启动成功后刷新 http://localhost:8070,可以看到 spring-cloud-provider 服务有两个实例(相当于一个小集群)。

image

创建服务消费者

改造原先的 spring-cloud-consumer 工程

pom.xml 添加 Eureka Client 依赖

<!-- 引入 eureka client 依赖-->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改 application.properties 文件,添加以下配置:

# 指定服务名称
spring.application.name=spring-cloud-consumer
# 指定运行端口
server.port=8073
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka

启动类

@EnableEurekaClient
@SpringBootApplication
public class ConsumerApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConsumerApplication.class, args);
	}
	
}

之前是通过服务接口的具体地址来调用的,既然用了注册中心,那么客户端调用的时候肯定是不需要关心有多少个服务提供接口,下面我们来改造之前的调用代码。

首先改造 RestTemplate 的配置,添加一个 @LoadBalanced 注解,这个注解会自动构造 LoadBalancerClient 接口的实现类并注册到 Spring 容器中,代码如下所示。

@Configuration
public class BeanConfiguration {

	@Bean
	@LoadBalanced
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
}

接下来就是改造调用代码,我们不再直接写固定地址,而是写成服务的名称,这个名称就是我们注册到 Eureka 中的名称,是属性文件中的 spring.application.name,相关代码如下所示。

@RestController
public class UserController {

	@Autowired
	private RestTemplate restTemplate;
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name) {
		return restTemplate.getForObject("http://spring-cloud-provider/users/" + name, String.class);
	}
}

在 Eureka 中,一个工程的 spring.application.name 的属性值对应着一个服务的 ID,Eureka 可以根据这个 ID 去调度服务,我们无需关心具体的 IP 和端口,只要知道服务提供者的名字就可以调用了。

启动 spring-cloud-consumer 工程,刷新 http://localhost:8070,可以看到服务消费者也注册到 Eureka 了。

image

访问服务消费者 http://localhost:8073/users/zhangsan ,如果返回 “hello zhangsan” 字符串,说明我们调用成功了。

源码下载:https://github.com/miansen/SpringCloud-Learn

一、Spring Cloud-服务提供者与服务消费者

2019-09-20   龙德   SpringCloud   SpringCloud  

本篇以及后面的系列文章都是使用以下环境:

  • JDK 版本:1.8

  • IDA:Eclipse 4.6.0

  • Maven 版本:3.5.0

  • SpringBoot 版本:2.1.8.RELEASE

  • SpringCloud 版本:Greenwich.RELEASE

父工程

创建一个父工程,命名为 spring-cloud-pom, 所有的子工程都继承这个父工程,用以管理子工程的版本依赖。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	
	<!-- Spring Boot -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.8.RELEASE</version>
		<relativePath/>
	</parent>
	
	<groupId>com.example</groupId>
	<artifactId>spring-cloud-pom</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>pom</packaging>
	<name>spring-cloud-pom</name>

	<properties>
		<java.version>1.8</java.version>
		<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
	</properties>
	
	<!-- Spring Cloud -->
	<dependencyManagement>
	   <dependencies>
	       <dependency>
	           <groupId>org.springframework.cloud</groupId>
	           <artifactId>spring-cloud-dependencies</artifactId>
	           <version>${spring-cloud.version}</version>
	           <type>pom</type>
	           <scope>import</scope>
	       </dependency>
	   </dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

父工程只留一个 pom.xml 文件就可以了,其他的文件目录都可以不要。

服务提供者

在父工程下新建一个子工程 spring-cloud-provider,作为服务提供者。

pom.xml

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  
  <parent>
    <groupId>com.example</groupId>
    <artifactId>spring-cloud-pom</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  
  <artifactId>spring-cloud-provider</artifactId>
  <name>spring-cloud-provider</name>
  <url>http://maven.apache.org</url>
  
  <dependencies>
  	<!-- 引入 web 依赖 -->
  	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
  </dependencies>
</project>

application.properties

# 服务名字
spring.application.name=spring-cloud-provider
# 在 8071 端口启动
server.port=8071

controller

创建一个 Controller,提供一个接口给其他服务查询,简单的输出一些信息即可。

@RestController
public class UserController {

	@GetMapping("/users/{name}")
	public String users(@PathVariable("name") String name) {
		return "hello " + name;
	}
}

启动服务,访问 http://localhost:8071/user/zhangsan ,如果能看到我们返回的 “hello zhangsan” 字符串,就证明接口提供成功了。

服务消费者

在父工程下新建一个子工程 spring-cloud-consumer,作为服务消费者。

pom.xml

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  
  <parent>
    <groupId>com.example</groupId>
    <artifactId>spring-cloud-pom</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  
  <artifactId>spring-cloud-consumer</artifactId>
  <name>spring-cloud-consumer</name>
  <url>http://maven.apache.org</url>
  
  <dependencies>
  	<!-- 引入 web 依赖 -->
  	<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
  </dependencies>
</project>

application.properties

# 服务名字
spring.application.name=spring-cloud-consumer
# 在 8072 端口启动
server.port=8072

RestTemplate

RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。我们通过配置 RestTemplate 来调用接口。

@Configuration
public class BeanConfiguration {

	@Bean
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
}

controller

创建 controller,利用 RestTemplate 访问服务提供者的接口。

@RestController
public class UserController {

	@Autowired
	private RestTemplate restTemplate;
	
	@GetMapping("/users/{name}")
	public String getUser(@PathVariable("name") String name) {
		return restTemplate.getForObject("http://localhost:8071/users/" + name, String.class);
	}
}

服务消费者的 getUser() 方法没有自己实现,而是调用服务提供者的接口。

启动服务消费者,访问 http://localhost:8072/users/zhangsan ,如果返回 “hello zhangsan” 字符串就证明调用成功了。

这样一个简单的伪微服务项目的服务提供者和消费者就已经完成了。

之所以说是伪微服务,是因为服务提供者和服务消费者其实是两个独立的应用,它们之间只是简单的通过 http 的方式进行资源访问和操作。而真正微服务架构不仅仅是将单体应用划分为小型的服务单元这么简单,还需要一套的基础组件来管理各个服务,如服务注册、服务发现、配置中心、消息总线、负载均衡、断路器、数据监控等。Spring Cloud 就是这一系列微服务基础组件框架的集合,利用 Spring Boot 的开发便利性,巧妙地简化了微服务系统基础设施的开发。

通俗地讲,Spring Cloud 就是用于构建微服务开发和治理的框架集合(它并不是具体的一个框架,而是框架集合)

Spring Cloud 提供的微服务基础组件有:

  • Eureka:服务注册中心,用于服务管理
  • Ribbon:基于客户端的负载均衡组件
  • Hystrix:容错框架,能够防止服务的雪崩效应
  • Feign:Web 服务客户端,能够简化 HTTP 接口的调用
  • Zuul:API 网关,提供路由转发、请求过滤等功能
  • Config:分布式配置管理
  • Sleuth:服务跟踪
  • Stream:构建消息驱动的微服务应用程序的框架
  • Bus:消息代理的集群消息总线

所以接下来,我们利用 Spring Cloud 提供的组件,一步步的构建出一个真正的微服务系统(Demo)。

源码下载:https://github.com/miansen/SpringCloud-Learn

Java ==、equals() 和 hashCode()

2019-09-11   龙德   Java   equals hashCode  

==

对于基本类型,== 比较的是字面量。

对于引用类型,== 比较的是地址值。

String a = new String("abc");
String b = new String("abc");
String aa = "abc";
String bb = "abc";
// false,因为用 new 关键字创建 String 对象,每次都是新对象,地址值都不同
System.out.println(a == b);
// true,因为用字符串字面量创建 String 对象,都指向位于 Srping pool 中的同一个对象,地址值相同
System.out.println(aa == bb);
// true,基本类型比较的是字面量
System.out.println(42 == 42.0);

equals()

分两种情况

1.没有重写 equals(),则比较的是地址值,相当于 == 比较。

public class Test03 {

	public static void main(String[] args) {
		User user1 = new User("张三",15);
		User user2 = new User("张三",15);
		// false,因为 User 类没有重写 equals(),实际还是通过 == 比较
		System.out.printf("user1.equals(user2): %s \n",user1.equals(user2));
	}
	
	public static class User{
		String name;
		int age;
		public User(String name, int age) {
			this.name = name;
			this.age = age;
		}
	}
}

看一下 Object 中的 equals()

public boolean equals(Object obj) {
    return (this == obj);
}

可以看到用的是 == 比较

2.重写了 equals()

我们可以在 equals() 里面先比较两个对象是否相等,如果不相等,再去比较内容是否相等。

public class Test03 {

	public static void main(String[] args) {
		User user1 = new User("张三",15);
		User user2 = new User("张三",15);
		// true,因为重写了 equals()
		System.out.printf("user1.equals(user2): %s \n",user1.equals(user2));
	}
	
	public static class User{
		String name;
		int age;
		public User(String name, int age) {
			this.name = name;
			this.age = age;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			User other = (User) obj;
			if (age != other.age)
				return false;
			if (name == null) {
				if (other.name != null)
					return false;
			} else if (!name.equals(other.name))
				return false;
			return true;
		}
	}
}

String 类也重写了 equals(),所以我们可以直接拿来比较。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                        return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

hashCode()

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

通俗的将,在 Java 中,你要在本质是散列表的集合中用到该对象,如:HashMap,Hashtable,HashSet,那么 hashCode() 才起作用,其他情况下没用。

1.一般情况,只重写 equals(),没有重写 hashCode()

public class Test03 {

	public static void main(String[] args) {
		User user1 = new User("张三",15);
		User user2 = new User("张三",15);
		System.out.printf("user1.equals(user2): %s, user1 hashCode: %d, user2 hashCode: %d \n",user1.equals(user2),user1.hashCode(),user2.hashCode());
		
	}
	
	public static class User{
		
		String name;
		
		int age;
		
		public User(String name, int age) {
			this.name = name;
			this.age = age;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			User other = (User) obj;
			if (age != other.age)
				return false;
			if (name == null) {
				if (other.name != null)
					return false;
			} else if (!name.equals(other.name))
				return false;
			return true;
		}
		
	}
}

输出:

user1.equals(user2): true, user1 hashCode: 382503496, user2 hashCode: 721139189 

可以看到 hashCode 不相等,并不影响 equals() 比较。

2.往 HashSet 中添加该对象

User user1 = new User("张三",15);
User user2 = new User("张三",15);
System.out.printf("user1.equals(user2): %s, user1 hashCode: %d, user2 hashCode: %d \n",user1.equals(user2),user1.hashCode(),user2.hashCode());
HashSet<User> set = new HashSet<>();
set.add(user1);
set.add(user2);
System.out.println(set);

输入:

user1.equals(user2): true, user1 hashCode: 1936129502, user2 hashCode: 365113514 
[User [name=张三, age=15], User [name=张三, age=15]]

可以看到这两个对象的内容是重复的,因为往 HashSet 中添加该对象时,会计算对象的 hashCode 值来判断对象加入的位置是否相同,如果不相同,那么就认为这两个对象不相等。

所以虽然 user1 和 user2 的内容相等,但是它们的 hashCode 不相等。所以 HashSet 在添加它们时,会认为它们不相等。

3.重写 HashCode()

@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + age;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}

输出:

user1.equals(user2): true, user1 hashCode: 776315, user2 hashCode: 776315 
[User [name=张三, age=15]]

可以看到 user1 和 user2 的 hashCode 相等,往 HashSet 中添加它们时,首先会根据 hashCode 判断对象加入的位置是否相同,如果相同,那么就再去判断它们的内容是否一样,如果一样,那么会认为它们是相等的。

总结

1.对于基本类型,== 比较的是字面量

2.对于引用类型,== 比较的是地址值

3.没有重写 equals(),则比较的是地址值,相当于 == 比较

4.重写 equals(),则比较内容是否相等

5.对象相等,hashCode() 一定相等

6.hashCode() 相等,对象不一定相等

7.对象相等,equals() 一定相等

8.equals() 相等,对象不一定相等

9.equals() 和 hashCode() 没有关系

10.在本质是散列表的集合中使用某个对象,则必须重写该对象的 equals() 和 hashCode() 方法

Java反射-Method的Invoke方法

2019-09-05   龙德   Java   反射 Method  

一个简简单单的 User 类

public class User {

	private String name;

	private int age;

	public User() {
	}

	public User(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public void print() {
		System.out.println("name: " + this.name + ", age: " + this.age);
	}

	public void print(String sex, String addr) {
		System.out.println("name: " + this.name + ", age: " + this.age + ", sex: " + sex + ", addr: " + addr);
	}

	private void privateMethod() {
		System.out.println("private 方法");
	}

	protected void protectedMethod() {
		System.out.println("protected 方法");
	}

	void defaultMethod() {
		System.out.println("default 方法");
	}

	public static void staticMethod() {
		System.out.println("static 方法");
	}

	public final void finalMethod() {
		System.out.println("final 方法");
	}
}

测试类

public class UserTest {

	public static void main(String[] args) throws Exception {
		// 1.先获取 User 类的 Class 对象
		Class userClass = User.class;
		
		// 2.通过 Class 对象获取 Method 对象,参数是方法名(String methodName)和参数类型(Class<?>... parameterTypes)
		
		// public 修饰的空参方法
		Method print1 = userClass.getMethod("print");
		
		// public 修饰的带参方法
		Method print2 = userClass.getMethod("print", String.class, String.class);
		
		// private 修饰的方法,抛出异常 java.lang.NoSuchMethodException
		// Method privateMethod = userClass.getMethod("privateMethod", null);
		
		// protected 修饰的方法,抛出异常 java.lang.NoSuchMethodException
		// Method protectedMethod = userClass.getMethod("protectedMethod", null);
		
		// default 修饰的方法,抛出异常 java.lang.NoSuchMethodException
		// Method defaultMethod = userClass.getMethod("defaultMethod", null);
		
		// static 方法
		Method staticMethod = userClass.getMethod("staticMethod", null);
		
		// final 方法
		Method finalMethod = userClass.getMethod("finalMethod", null);
		
		// 3.调用 Method 对象的 invoke 方法,将 User 类的实例对象和方法的参数传进去
		User user1 = new User("ZhangSan",10);
		User user2 = new User("LiSi",12);
		print1.invoke(user1);
		print2.invoke(user2,"男","中国");
		staticMethod.invoke(user2);
		finalMethod.invoke(user2);
		
		// 一般的方法调用
		System.out.println("========= 一般的方法调用 ========");
		user1.print();
		user2.print("男","中国");
		user2.staticMethod();
		user2.finalMethod();
	}
}

控制台输出

name: ZhangSan, age: 10
name: LiSi, age: 12, sex: 男, addr: 中国
static 方法
final 方法
========= 一般的方法调用 ========
name: ZhangSan, age: 10
name: LiSi, age: 12, sex: 男, addr: 中国
static 方法
final 方法

可以看到通过 Method 对象调用的 invoke() 方法跟对象.方法名的调用的结果是一样的。

结论

终于明白 JDK 的动态代理为什么能执行原对象的方法了,原来是将 Method 对象和方法参数 args 传进了 InvocationHandler 接口的 invoke(Object proxy, Method method, Object[] args) 方法里,然后通过调用 method.invoke(原对象实例,args) 方法就可以达到调用原对象方法的效果。

public class ObjInterceptor implements InvocationHandler {

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			return method.invoke(obj, args);
		}
	}

Elasticsearch-集群

2019-08-25   龙德   Elasticsearch   Elasticsearch  
环境准备,请确保你已经有了以下环境,(1)3台虚拟机,我是用 VMware 搭建的虚拟机,每台虚拟机的地址如下:192.168.8.4、192.168.8.6、192.168.8.8(2)每台虚拟机都确保安装好了 Elasticsearch

Elasticsearch-Java客户端

2019-08-02   龙德   Elasticsearch   Elasticsearch  
Elasticsearch 官方提供了3种客户端:1.TransportClient,这个客户端在7.0.0弃用,将在8.0版本中删除。2.elasticsearch-rest-client,低级的REST客户端,基于Apache HTTP,支持所有Elasticsearch版本,请求编码和响应解码留给用户实现(自己写JSON)。3.elasticsearch-rest-high-level-client,高级的REST客户端,基于低级客户端,对增删改查进行了封装,提供特定方法的API,不需要用户处理编解码,类似之前的TransportClient,但是兼容性较差,对客户端和集群版本要求较高。

Elasticsearch-基础

2019-07-20   龙德   Elasticsearch   Elasticsearch  

Elasticsearch 是一个 nosql 数据库,它底层基于开源库 Lucene,Elasticsearch 是 Lucene 的封装,隐藏了复杂的操作,提供了 RESTful 风格的 API 和 Java 客户端(其它语言的客户端也有),开箱即用。

数据结构-单向链表

2019-07-08   龙德   数据结构   数据结构 单向链表  

链表的定义

链表是线性表的链式存储结构,由 n 个节点组成,每个节点包含两个元素:数据域和指针域