天天看點

【Spring Cloud總結】3.服務提供者與服務消費者

在開始學習使用SpringCloud進行開發之前,我們先來了解一下什麼是“服務提供者”和“服務消費者”,然後分别編寫一個簡單的“服務提供者”和“服務消費者”。

一、概念

我們拿第一篇博文《【Spring Cloud總結】1.微服務架構概述》中的電影銷售系統的架構來對服務提供者和消費者進行一個定位:

【Spring Cloud總結】3.服務提供者與服務消費者

可以看到,當使用者通路電影購票系統時,首先通路的是前置系統“電影微服務”,而電影微服務在進行出票的過程中,需要擷取使用者之前注冊的使用者資訊,此時就需要調用使用者微服務的服務,來擷取使用者資訊。這裡電影微服務就是“服務消費者”,而使用者微服務就是“服務提供者”,電影微服務需要消費使用者查詢服務,而使用者微服務提供了使用者查詢服務。

有關“服務消費者”和“服務提供者”的定義:

【Spring Cloud總結】3.服務提供者與服務消費者

二、編寫一個服務提供者

首先我們來編寫一個簡單的服務提供者樣例工程。這裡我們直接通過Spring官網提供的建構工具來建立一個SpringBoot工程。打開https://start.spring.io/:

【Spring Cloud總結】3.服務提供者與服務消費者

這裡填寫我們需要的資訊,其中系統建構使用Maven,程式設計語言使用Java,SpringBoot使用最新2.2.0(SNAPSHOT快照)版本,Group和Artifact定義為微服務Demo的相關資訊:“com.microserver.cloud”、“microserver-simple-provider-user”。

最後需要一些依賴,web、jpa以及H2:

【Spring Cloud總結】3.服務提供者與服務消費者

其中web是我們需要的Spring-web,用于支援Web開發;jpa是Spring-jpa是持久層架構,用于操作資料庫;H2是H2資料庫,内置資料庫,用來做一些資料的展示。

點選“Generate Project”按鈕,來建構一個基于Spring boot的一個Project。此時會彈出一個Zip壓縮包供我們下載下傳,下載下傳完畢後得到一個檔案:

【Spring Cloud總結】3.服務提供者與服務消費者

我們将該壓縮檔案解壓,準備導入IDE。

打開Spring Tool Suite,這裡JDK和Maven按照之前的約定配置好,點選Import将上面的工程導入進去:

【Spring Cloud總結】3.服務提供者與服務消費者
【Spring Cloud總結】3.服務提供者與服務消費者
【Spring Cloud總結】3.服務提供者與服務消費者

導入後,我們可以看到工程的整個結構:

【Spring Cloud總結】3.服務提供者與服務消費者

我們檢視一下POM檔案:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.BUILD-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.microserver.cloud</groupId>
    <artifactId>microserver-simple-provider-user</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>microserver-simple-provider-user</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

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

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </pluginRepository>
    </pluginRepositories>

</project>
           

可以看到,SpringBoot的版本就使我們之前選擇的2.2.0-SNAPSHOT版本,然後也引入了web、jpa和H2的相關依賴。最上面的src下,生成了一個預設的MicroserverSimpleProviderUserApplication.java類:

package com.microserver.cloud.microserversimpleprovideruser;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MicroserverSimpleProviderUserApplication {

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

}
           

該類用于加載預設啟動項,并啟動SpringBoot工程。

下面開始開發服務提供者。首先我們編寫一個SQL檔案用于建表,在src/main/resource下建立一個schema.sql,存儲建表語句:

【Spring Cloud總結】3.服務提供者與服務消費者

在schema.sql中編寫建表語句:

DROP TABLE USER IF EXISTS;
CREATE TABLE USER(
    id BIGINT generated BY DEFAULT AS identity,  -- h2資料庫自增主鍵文法
    username VARCHAR(40) COMMENT '賬号',
    NAME VARCHAR(20) COMMENT '姓名',
    age INT(3) COMMENT '年齡',
    balance DECIMAL(10,2) COMMENT '餘額',
    PRIMARY KEY(id)
);
           

然後建立一個data.sql,用于給user表插入測試資料:

在data.sql中編寫插入資料:

INSERT INTO USER(id,username,NAME,age,balance) VALUES(1,'user1','張三',20,100.00);
INSERT INTO USER(id,username,NAME,age,balance) VALUES(2,'user2','李四',21,200.00);
INSERT INTO USER(id,username,NAME,age,balance) VALUES(3,'user3','王五',22,300.00);
INSERT INTO USER(id,username,NAME,age,balance) VALUES(4,'user4','趙六',23,400.00);
           

資料準備好之後,我們在src/main/java下的com.microserver.cloud.entity包下建立一個User實體類:

package com.microserver.cloud.entity;

import java.math.BigDecimal;

public class User implements Serializable{
    private Long id;
    private String username;
    private String name;
    private Short age;
    private BigDecimal balance;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Short getAge() {
        return age;
    }
    public void setAge(Short age) {
        this.age = age;
    }
    public BigDecimal getBalance() {
        return balance;
    }
    public void setBalance(BigDecimal balance) {
        this.balance = balance;
    }
}
           

由于我們的項目是使用SpringData-JPA的,是以需要加上JPA的注解:

package com.microserver.cloud.entity;

import java.math.BigDecimal;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User implements Serializable{
    @Id  //标記為主鍵
    @GeneratedValue(strategy=GenerationType.AUTO) //主鍵自增
    private Long id;
    @Column //标記為資料庫字段,下同
    private String username;
    @Column
    private String name;
    @Column
    private Short age;
    @Column
    private BigDecimal balance;
        //Get與Set方法省略
}
           

然後編寫DAO層,在src/main/java下的com.microserver.cloud.dao包下建立一個UserRepository接口:

package com.microserver.cloud.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import com.microserver.cloud.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long>{

}
           

這裡方法體不需要實作其他方法定義,JpaRepository父接口已經定義了常用的增删改查操作方法:

package org.springframework.data.jpa.repository;

import java.util.List;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;

@NoRepositoryBean
public abstract interface JpaRepository<T, ID>
  extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T>
{
  public abstract List<T> findAll();
  
  public abstract List<T> findAll(Sort paramSort);
  
  public abstract List<T> findAllById(Iterable<ID> paramIterable);
  
  public abstract <S extends T> List<S> saveAll(Iterable<S> paramIterable);
  
  public abstract void flush();
  
  public abstract <S extends T> S saveAndFlush(S paramS);
  
  public abstract void deleteInBatch(Iterable<T> paramIterable);
  
  public abstract void deleteAllInBatch();
  
  public abstract T getOne(ID paramID);
  
  public abstract <S extends T> List<S> findAll(Example<S> paramExample);
  
  public abstract <S extends T> List<S> findAll(Example<S> paramExample, Sort paramSort);
}
           

這個例子很簡單,我們這裡就不再編寫Service層了,直接Controller層調用DAO層。我們在src/main/java下的com.microserver.cloud.controller包下建立一個UserController類:

package com.microserver.cloud.controller;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.microserver.cloud.dao.UserRepository;
import com.microserver.cloud.entity.User;

@RestController
public class UserController {
    @Autowired
    private UserRepository userDao;
    
    @GetMapping("/findById/{id}")
    public User findById(@PathVariable Long id){
        Optional<User> userOptional = this.userDao.findById(id);
        return userOptional.get();
    }
}
           

其中@RestController定該Controller對外暴露REST風格的服務,@RestController為@Controller與@ResponseBody的組合注解,旨在提供json格式的REST服務。

然後使用@Autowired注解将UserRepository資料庫操作類注入;然後建立findById方法,參數為id,使用@PathVariable旨在映射URL請求路徑上的“{id}”;調用userDao.findById方法,得到一個被包裝為Optional操作類的對象,調用get()方法擷取User類本身。

最後使用@GetMapping注解來修飾findById方法,@GetMapping注解實際上是RequestMapping的GET寫法,定義一個GET類型的HTTP服務(Spring4.3後提供):

@RequestMapping(method={RequestMethod.GET})。

編寫完邏輯層之後,我們來進行一些配置。工程中原本提供了properties檔案進行配置,這裡我們推薦使用yml檔案進行配置,因為yml是一個比較結構化的語言風格。是以我們删除原來的application.properties檔案,建立application.yml檔案:

【Spring Cloud總結】3.服務提供者與服務消費者

在application.yml檔案中加入以下配置:

server:
  port: 7900
spring:
  jpa:
    generate-ddl: false
    show-sql: true
    hibernate:
      ddl-auto: none
  datasource:
    platform: h2
    schema: classpath:schema.sql
    data: classpath:data.sql
logging:
  level:
    root: info
    org.hibernate: INFO
    org.hibernate.type.descripter.sql.BasicBinder: TRACE
    org.hibernate.type.descripter.sql.BasicExtractor: TRACE
    com.microserver: DEBUG
           

其中:

“server.port”定義了服務啟動的端口;

“spring.jpa.generate-ddl”為啟動時是否生成DDL語句,這裡我們已經自己編寫好了,是以false不生成;

“spring.jpa.show-sql”為是否列印SQL語句,這裡設定為true;

“spring.jpa.hibernate.ddl-auto”為hibernate每次啟動時是否自動建立表單(jpa依賴hibernate),這裡要求啟動時不做DDL的處理,是以設定為none;

“spring.datasource.plaform”用于設定資料源使用的資料庫類型,這裡為H2;

“spring.datasource.schema”用于設定資料庫啟動時的建表語句檔案位置;

“spring.datasource.data”用于設定資料庫啟動時的資料庫資訊插入語句檔案位置;

“logging.level.root”用于設定根目錄的日志級别;

“logging.level.org.hibernate”用于設定hibernate的日志級别;

“logging.level.type.descripter.sql.XXX”用于設定hibernate輸出SQL語句到日志,與上面的“spring.jpa.show-sql”配合。

“logging.level.com.microserver”設定我們自己業務代碼的日志級别為DEBUG,用于展現細粒度的日志,友善查找Bug。

注:沒有yml編輯插件的同學,在Eclipse中安裝yml插件詳見:

https://www.cnblogs.com/mmzs/p/9493915.html

這裡需要将MicroserverSimpleProviderUserApplication移到com.microserver.cloud包中,即是在SpringBoot中有個約定俗成的規矩,因為Application要掃描子包的注解,是以要求Application啟動類要在業務包的最頂層:

【Spring Cloud總結】3.服務提供者與服務消費者

所有準備工作完畢,我們來啟動該工程。執行MicroserverSimpleProviderUserApplication啟動類的main方法:

【Spring Cloud總結】3.服務提供者與服務消費者

控制台次最後出現MicroserverSimpleProviderUserApplication : Started字樣,即啟動成功。

我們在浏覽器中通路剛剛編寫的服務,來擷取id為1的使用者資訊:

【Spring Cloud總結】3.服務提供者與服務消費者

可以看到通路成功,我們的服務提供者編寫成功。

二、編寫一個服務消費者

我們來編寫一個服務消費者來調用服務提供者的服務。新建構一個名為“microserver-simple-consumer-movie”(Artifact同名)的工程,和之前的建構方式一樣(外部依賴隻需要web,jpa和h2不需要),這裡不再贅述。

User類和microserver-simple-provider-user中的一樣,這是要把有關JPA的所有注解去除:

package com.microserver.cloud.entity;

import java.io.Serializable;
import java.math.BigDecimal;


public class User implements Serializable{
	private Long id;
	private String username;
	private String name;
	private Short age;
	private BigDecimal balance;
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Short getAge() {
		return age;
	}
	public void setAge(Short age) {
		this.age = age;
	}
	public BigDecimal getBalance() {
		return balance;
	}
	public void setBalance(BigDecimal balance) {
		this.balance = balance;
	}
}
           

我們編寫一個MovieController,用來調用服務提供者的服務:

package com.microserver.cloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.microserver.cloud.entity.User;

@RestController
public class MovieController {
    
    @Autowired
    private RestTemplate restRemplate;
    
    @GetMapping("/movie/{id}")
    public User findUserById(@PathVariable Long id){
        return this.restRemplate.getForObject("http://localhost:7900/findById/"+id, User.class);
    }
}
           

這裡使用RestTemplate類來調用Http服務,使用的是getForObject,分别傳入請求的HTTP服務的URL、傳回的實體類類型。

由于RestTemplate需要開發人員來配置其構造函數的參數SimpleClientHttpRequestFactory對象,是以這裡隻寫@Autowired不進行配置,是不能自動注入的。建立RestTemplateConfig類,添加@Configuration作為配置類,類似在XML中去編寫了RestTemplate類的<bean>注入配置:

package com.microserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
 
/**
 * RestTemplate配置
 * 這是一種JavaConfig的容器配置,用于spring容器的bean收集與注冊,并通過參數傳遞的方式實作依賴注入。
 * "@Configuration"注解标注的配置類,都是spring容器配置類,springboot通過"@EnableAutoConfiguration"
 * 注解将所有标注了"@Configuration"注解的配置類,"一股腦兒"全部注入spring容器中。
 *
 */
@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        return new RestTemplate(factory);//在Spring容器中注入RestTemplate對象
    }
 
    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        //初始化RestTemplate對象需要的Factory工廠類,biang注入到Spring容器中
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(5000);//讀取回報逾時時間5000ms
        factory.setConnectTimeout(15000);//連接配接逾時時間15000ms
        return factory;
    }
}
           

然後在src/main/resource下建立一個application.yml,裡面隻需要配置端口号就行了:

server:
  port: 7901
           

最後别忘記把MicroserverSimpleConsumerMovieApplication類移到包的最頂層。

分别先後啟動服務提供者和服務消費者,然後通路服務消費者的movie請求:

【Spring Cloud總結】3.服務提供者與服務消費者

拿到了userId為1的使用者資訊。至此我們的消費者建立成功并且成功消費了服務提供者提供的服務資訊。

三、總結

之是以花時間來編寫上面兩個服務,主要是照顧之前沒有學習過SpringBoot的小夥伴們。

這隻是一個小小的例子,其實該例子中有很多缺陷。例如服務提供者的路徑是寫死(寫死)的:

this.restRemplate.getForObject("http://localhost:7900/findById/"+id, User.class);
           

這種情況下,在微服務環境,ip、端口都是動态的,至少要進行一個IP配置,是以這裡修改為:

package com.microserver.cloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.microserver.cloud.entity.User;

@RestController
public class MovieController {
    
    @Autowired
    private RestTemplate restRemplate;

        @Values("${user.userServicePath}")
        private String userServicePath;

    @GetMapping("/movie/{id}")
    public User findUserById(@PathVariable Long id){
        return this.restRemplate.getForObject(this.userServicePath+id, User.class);
    }
}
           

在application.yml中添加

user:
  userServicePath: http://localhost:7900/findById/
           

而即使配置了IP,在微服務運作在類似Docker容器中的時候,也有可能會頻繁出現ip和端口的變化,每次變化的時候,都需要修改配置檔案重新開機,也不利于維護。

而且在叢集環境下,一個服務提供者可能有多個執行個體,ip和端口各不同,無法在系統裡直接配置,當然,使用Nginx負載均衡可以通過通路代理位址,來達到随機通路相關某個執行個體的需要,但是當分布式系統過于龐大時,單一的Nginx節點已經不能滿足需求了,配置很多個Nigix代理也是一項繁雜且不易維護的操作,此時我們就需要一個能夠動态提供服務ip和端口的進行中心,這一點下一節我們來探讨。

參考:《51CTO學院Spring Cloud進階視訊》

轉載請注明出處:https://blog.csdn.net/acmman/article/details/90718542

繼續閱讀