天天看点

【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

继续阅读