Eureka註冊中心在微服務架構中是必不可少的一部分,主要用來實現服務治理功能,總之,很重要。
如果需要看懂後面的文檔,需要有一定的Spring Boot和maven的基礎。
使用Eureka編寫註冊中心服務
這裏使用Maven多模塊來管理項目,maven多模塊這裏就不多說了。Spring cloud版本爲:2.2.0.RELEASE,Spring boot版本爲:2.2.1.RELEASE,Spring版本爲:5.2.1.RELEASE。
父模塊:demo、三個子模塊:demo-eureka(註冊中心)、demo-provider(服務提供者)、demo-client(服務調用者)。
demo-eureka
- 添加依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
- 啓動類EurekaApplication
package com.demo.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
@EnableEurekaServer:表示這個應用作爲註冊中心
- 配置文件application.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
client:
register-with-eureka: false
fetch-registry: false
本應用爲註冊中心,所以設置爲false,表示不向註冊中心註冊自己
eureka.client.register-with-eureka = false
註冊中心的職責就是維護服務實例,它並不需要去檢索服務
eureka.client.fetch-registry = false
- 啓動
啓動類右鍵 >> debug,啓動完成後,瀏覽器訪問http://127.0.0.1:8761,當看到下面頁面,就說明註冊中心已經搭建完畢。
demo-provider
這裏使用JPA實現對錶user_info的增刪改查,只是一個簡單的demo
- 添加依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
Lombok:省略getter、setter等,簡化實體類,網上自己找
druid:alibaba數據源
驅動:mysql 和 postgresql都加進來了
- 啓動類ProviderApplication
package com.demo.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
@EnableDiscoveryClient:當前服務是一個Eureka的客戶端
- 配置文件application.yml
server:
port: 9000
spring:
application:
name: demo-provider
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:postgresql://127.0.0.1:5432/postgres
driver-class-name: org.postgresql.Driver
username: postgres
password: 123456
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
temp:
use_jdbc_metadata_defaults: false
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka/
數據源配置不同多說
spring.datasource.*
簡單理解爲,不用手動建表,表會自動創建或更新
spring.jpa.hibernate.ddl-auto = update
這個地址指向註冊中心,多個用“,”隔開
eureka.client.service-url.defaultZone = http://127.0.0.1:8761/eureka/
- 新建實體類User
package com.demo.provider.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Data
@Entity
@Table(name = "user_info")
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
public class User implements Serializable {
@Id
@Column(name = "id", length = 32)
@GenericGenerator(name = "uuidGenerator", strategy = "uuid")
@GeneratedValue(generator = "uuidGenerator")
private String userId;
@Column(name = "name", length = 50)
private String name;
@Column(name = "sex")
private Integer sex;
@Column(name = "age")
private Integer age;
@Column(name = "birthday")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date birthday;
}
這裏使用了Lombok,所有沒有getter、setter,使用@Data就行
@JsonIgnoreProperties(value = { “hibernateLazyInitializer”}):直接將這個對象返回到客戶端段會有問題,所以需要加上這個註解
@JsonFormat:將這個對象返回到客戶端時,這個時間會格式化字符串。
@DateTimeFormat:客戶端傳值,用這個對象接受的話,傳的是一個格式化後的時間字符串
- Repository
package com.demo.provider.repository;
import com.demo.provider.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, String> {
}
- Controller,一個簡單的增刪改查
import com.demo.provider.entity.User;
import com.demo.provider.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping
public Page<User> findUser(@RequestParam(required = false, defaultValue = "1") Integer pageNo
, @RequestParam(required = false, defaultValue = "10") Integer size) {
return userRepository.findAll(PageRequest.of(pageNo, size));
}
@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
public User save(@RequestBody User user) {
userRepository.save(user);
return user;
}
@GetMapping("/{id}")
public User findById(@PathVariable String id) {
return userRepository.getOne(id);
}
@DeleteMapping("/{id}")
public String deleteById(@PathVariable String id) {
userRepository.deleteById(id);
return id;
}
}
- 啓動
項目啓動,測試接口,這裏記得先啓動註冊中心。啓動成功後,再訪問註冊中心。
REST風格http://127.0.0.1:9000/user,測試接口
demo-client
- 添加依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- 新建實體類User,爲什麼需要新家,就是服務提供者的User需要用到JPA
package com.demo.client.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
@Data
public class User implements Serializable {
private String userId;
private String name;
private Integer sex;
private Integer age;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date birthday;
}
- 請求模板
package com.demo.client.conf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class BeanConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
RestTemplate對象可以很方便的使用Java代碼發送http請求,所以創建一個RestTemplate,使用它來調用服務提供者提供的接口。
- UserController
package com.demo.client.controller;
import com.demo.client.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
public Object findUser(@RequestParam(required = false, defaultValue = "1") Integer pageNo
, @RequestParam(required = false, defaultValue = "10") Integer size) {
Map<String, Object> map = new HashMap<>();
map.put("pageNo", pageNo);
map.put("size", size);
return restTemplate.getForObject("http://127.0.0.1:9000/user?pageNo={pageNo}&size={size}", Object.class, map);
}
@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
public User save(@RequestBody User user) {
return restTemplate.postForObject("http://127.0.0.1:9000/user", user, User.class);
}
@GetMapping("/{id}")
public User findById(@PathVariable String id) {
return restTemplate.getForObject("http://127.0.0.1:9000/user/{id}", User.class, id);
}
@DeleteMapping("/{id}")
public String deleteById(@PathVariable String id) {
restTemplate.delete("http://127.0.0.1:9000/user/{id}", id);
return id;
}
}
注入一個RestTemplate對象,然後使用其發送請求。
本來到這一步已經基本上算完成了,但是細心的同學還是會發現,使用RestTemplate直接去調用服務的提供者提供的接口好像並沒有用到註冊中心,並且請求地址直接用IP+端口的方式顯然不科學。所以這裏需要稍微調整一下。
RestTemplate
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class BeanConfiguration {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
構造RestTemplate 對象時,加上@LoadBalanced註解,然後調用接口的時候就可以使用應用名稱${spring.applicaiton.name},而不用直接使用IP+端口了
UserController調整後代碼如下:
import com.demo.client.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
public Object findUser(@RequestParam(required = false, defaultValue = "1") Integer pageNo
, @RequestParam(required = false, defaultValue = "10") Integer size) {
Map<String, Object> map = new HashMap<>();
map.put("pageNo", pageNo);
map.put("size", size);
return restTemplate.getForObject("http://demo-provider/user?pageNo={pageNo}&size={size}", Object.class, map);
}
@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
public User save(@RequestBody User user) {
return restTemplate.postForObject("http://demo-provider/user", user, User.class);
}
@GetMapping("/{id}")
public User findById(@PathVariable String id) {
return restTemplate.getForObject("http://demo-provider/user/{id}", User.class, id);
}
@DeleteMapping("/{id}")
public String deleteById(@PathVariable String id) {
restTemplate.delete("http://demo-provider/user/{id}", id);
return id;
}
}
開啓Eureka認證
註冊中心(demo-eureka)中添加spring-security的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
application設置用戶名密碼
spring:
security:
user:
name: admin
password: 123456
此時在訪問註冊中心就需要使用剛纔的用戶名密碼登錄了
Eureka註冊的時候,地址需要稍微修改一下
eureka:
client:
service-url:
defaultZone: http://admin:123456@127.0.0.1:8761/eureka/
格式爲:http://{用戶名}:{密碼}:@127.0.0.1:8761/eureka/
這裏需要注意的是,需要關閉CSRF。 這裏爲了簡化,直接修改啓動類。
package com.demo.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@SpringBootApplication
@EnableEurekaServer
@EnableWebSecurity
public class EurekaApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
super.configure(http);
}
}
Eureka高可用搭建
理解Eureka高可用很簡單,如:有三臺機器master、worker1、worker2,讓Eureka分別運行在三臺機器上,同時,自己向其他兩臺機器上的註冊中心註冊自己,好比:
master向worker1和worker2註冊
worker1向master和worker2註冊
worker2向master和worker1註冊
application-test.yml
server:
port: 8761
spring:
application:
name: eureka-server
security:
user:
name: admin
password: 123456
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
client:
register-with-eureka: false
fetch-registry: false
啓動命令
# master
java -jar demo-eureka.jar \
--spring.profiles.active=test \
--eureka.client.service-url.defaultZone=http://admin:123456@worker1:8761/eureka/,http://admin:123456@worker2:8761/eureka/
# worker1
java -jar demo-eureka.jar \
--spring.profiles.active=test \
--eureka.client.service-url.defaultZone=http://admin:123456@master:8761/eureka/,http://admin:123456@worker2:8761/eureka/
# worker2
java -jar demo-eureka.jar \
--spring.profiles.active=test \
--eureka.client.service-url.defaultZone=http://admin:123456@master:8761/eureka/,http://admin:123456@worker1:8761/eureka/
結果:
快速移除已經失效的服務
在實際的開發過程中,我們可能會不停的重啓服務,由於Eureka有自己的保護機制,故節點下線後,服務信息還是一直存在Eureka。我們可以通過增加一些配置讓移除的速度更快一點,當然只是在開發環境下使用,生產環境不推薦。
eureka:
instance:
lease-renewal-interval-in-seconds: 5 # 默認30s
lease-expiration-duration-in-seconds: 5 # 默認60s
client:
healthcheck:
enabled: true
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 5000 #默認60000毫秒
- 關閉自我保護
eureka.server.enable-self-preservation=false - 清理間隔
eureka.server.eviction-interval-tirner-in-ms=5000 - 開啓健康檢查
eureka.client.healthcheck.enabled=true - Eureka Client發送心跳給server的頻率
eureka.instance.lease-renewal-interval-in-seconds=5 - Eureka Server至上一次收到client的心跳之後,等待一下次心跳的超時時間,在這個時間內沒有收到下一次心跳,則移除該Instance
eureka.instance.lease-expiration-duration-in-seconds=5
服務的上下線監控
註冊中心demo-eureka,新增代碼
package com.demo.eureka.conf;
import org.springframework.cloud.netflix.eureka.server.event.*;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class EurekaStateChangeListener {
@EventListener
public void listener(EurekaInstanceCanceledEvent event) {
System.out.println("服務下線:" + event.getServerId() + event.getAppName());
}
@EventListener
public void listener(EurekaInstanceRegisteredEvent event) {
System.out.println("服務註冊:" + event.getInstanceInfo().getAppName());
}
@EventListener
public void listener(EurekaInstanceRenewedEvent event) {
System.out.println("服務續約:" + event.getServerId() + event.getAppName());
}
@EventListener
public void listener(EurekaRegistryAvailableEvent event) {
System.out.println("註冊中心啓動");
}
@EventListener
public void listener(EurekaServerStartedEvent event) {
System.out.println("Eureka server 啓動");
}
}