• SpringCloud常用组件基本用法


    [基础]SpringCloud文字介绍

    简单介绍

    随着互联网的发展,网站 应用的规模不断扩大,需求的激增吗,带来了技术上的革命,系统架构也在不断的演进。从以前的单一应用,到垂直拆分,再到分布式服务,再到SOA(面向服务的架构),再到微服务架构,今天的SpringCloud和另一个阿里的Dubbo都是微服务架构目前比较火的两个框架;Spring善于集成,这个大家都是知道的,把世界上最好的框架拿过来,集成到自己的项目中,SpringCloud也还是一样,把当前非常流行的技术整合到一起,就成就了今天的SpringCloud,

    比如他集成了以前是一家刻光盘后来转技术的公司的诸多技术:

    • Eureka:注册中心

    • Zuul :网关

    • Ribbon:负载均衡

    • Feign :远程服务调用

    • Hystrix:熔断器

    • ......以上部分也是我们玩SpringCloud的核心技术。还有诸多诸多......

    微服务的特点

    • 单一职责,微服务中每一个服务都是一个唯一的业务,也就是说一个服务只干一件事情;

    • 微服务服务拆分粒度很小,但是五脏俱全

    • 微服务一般向外暴露Rest风格服务接口api,不关心服务的技术实现,可以是Java,可以是其他语言

    • 每个服务之间互相独立,互不干扰:

    • 面向服务,提供Rest接口,使用什么技术无人干涉

    • 前后端分离,提供统一Rest接口,不必在为PC,移动端单独开发接口

    • 数据库分离,每个服务都是用自己的数据源

    • 部署独立,每个服务都是独立的组件,可复用,可替换,降低耦合,容易维护;

    [基础]远程调用方式

    无论是Dubbo还是SpringCloud都会涉及到服务间的远程调用,目前常见的服务远程调用方式就一下两种

    • RPC:Dubbo是其使用者,自定义数据格式,基于原生TCP通信,速度快,效率高

    • Http:Http其实是一种网络传输协议,也是基于TCP,但他规定了数据的传输格式,现在浏览器和服务端基本使用的都是Http协议,他也可以用来作为远程服务调用,缺点就是里面封装的数据过多,不信你打开浏览器,看F12里面的数据,是不是有很多字段比如请求头那一堆堆...

    蜻蜓点水RPC

    RPC,即 Remote Procedure Call(远程过程调用),是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,说得通俗一点就是:A计算机提供一个服务,B计算机可以像调用本地服务那样调用A计算机的服务,RPC调用流程图如下:

      

    想要了解详细的RPC实现,给大家推荐一篇文章:自己动手实现RPC

    蜻蜓点水Http

    Http协议:超文本传输协议,是一种应用层协议。规定了网络传输的请求格式、响应格式、资源定位和操作的方式等。但是底层采用什么网络传输协议,并没有规定,不过现在都是采用TCP协议作为底层传输协议。例如我们通过浏览器访问网站,就是通过Http协议。只不过浏览器把请求封装,发起请求以及接收响应,解析响应的事情都帮我们做了。如果是不通过浏览器,那么这些事情都需要自己去完成。

      

    RPC和Http的异同

    Http与RPC的远程调用非常像,都是按照某种规定好的数据格式进行网络通信,有请求,有响应。在这方面,两者非常相似,但是还是有一些细微差别。

    • RPC并没有规定数据传输格式,这个格式可以任意指定,不同的RPC协议,数据格式不一定相同。

    • Http中还定义了资源定位的路径,RPC中并不需要

    • 最重要的一点:RPC需要满足像调用本地服务一样调用远程服务,也就是对调用过程在API层面进行封装。Http协议没有这样的要求,因此请求、响应等细节需要我们自己去实现。

      • 优点:RPC方式更加透明,对用户更方便。Http方式更灵活,没有规定API和语言,跨语言、跨平台

      • 缺点:RPC方式需要在API层面进行封装,限制了开发的语言环境

    如何选择?

    既然两种方式都可以实现远程调用,我们该如何选择呢?

    • 速度来看,RPC要比http更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿,不过可以采用gzip压缩。

    • 难度来看,RPC实现较为复杂,http相对比较简单

    • 灵活性来看,http更胜一筹,因为它不关心实现细节,跨平台、跨语言。

    因此,两者都有不同的使用场景:

    • 如果对效率要求更高,并且开发过程使用统一的技术栈,那么用RPC还是不错的。

    • 如果需要更加灵活,跨语言、跨平台,显然http更合适

    微服务,更加强调的是独立、自治、灵活。而RPC方式的限制较多,因此微服务框架中,一般都会采用基于Http的Rest风格服务。

    Http的客户端工具的包装类RestTemplate

    上面我们已经确定微服务要使用Http,目前比较常用的Http客户端工具有如下几款:

    • HttpClient

    • OkHttp

    • URLConnection()

    以上三种就不详细说明了,因为Spring提供了一个ResTemplate模版 工具类对基于Http的客户端进行了封装,并且还支持序列化和反序列化,非常的nice,RestTemplate并没有规定Http的客户端类型,目前三种常用的三种都支持!

      

    [基础]服务的远程调用:提供者和消费者

    创建一个父工程,我们不做过多的依赖,就定义一下常用的配置,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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ccl.demo</groupId>
    <artifactId>cloud-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath/>
    </parent>

    <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.RC1</spring-cloud.version>
    </properties>

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

    <repositories>
    <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/milestone</url>
    <snapshots>
    <enabled>false</enabled>
    </snapshots>
    </repository>
    </repositories>
    </project>

    服务的提供者

    在父工程中创建一个Moudle,选择maven,因为我们已经有父亲工程了,整体架构如下:

       

    pom.xml内容:——>

        <dependencies>
            <!--Spring Boot-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
                <version>2.1.6.RELEASE</version>
            </dependency>
            <!--Web-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.1.6.RELEASE</version>
            </dependency>
            <!--阿里的Druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.10</version>
            </dependency>
            <!--mysql连接驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.16</version>
            </dependency>
            <!--简化set get 有参无参等的工具依赖-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.8</version>
            </dependency>
            <!--持久层框架 : JPA -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
                <version>2.1.6.RELEASE</version>
                <scope>compile</scope>
            </dependency>
        </dependencies>

    实体类:Employee ——>

    /*提供有参无参,get set*/
    @Data
    /*用于指定数据库表的名称,不指定默认类名*/
    @Entity(name = "test_employee")
    /*JPA底层使用Hibernate,采用延迟加载,返回代理对象在RestController想转Json时会报错,代理对象没有数据填充*/
    @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "fieldHandler"})
    public class Employee {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
        private String name;
        private String dbase;
    }

    持久层Repository——>

    /**
     * JpaRepository<Employee,Integer>
     * 第一个泛型为确定对象关系映射的类
     * 第二个泛型确定该类的主键类型
     */
    @Component
    public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
    }

    业务层Service——>

    @Service
    public class EmployeeServiceImpl implements EmployeeService {
        @Autowired
        private EmployeeRepository repository;
        
        @Override
        public boolean saveEmployee(Employee employee) {
            Employee employee1 = repository.save(employee);
            if (employee != null) {
                return true;
            }
            return false;
        }
        
        @Override
        public boolean removeEmployee(int id) {
            //Jpa的deleteById方法,如果id不存在就会抛出异常,在进行操作是,应先确定其存在
            if (repository.existsById(id)) {
                repository.deleteById(id);
                return true;
            }
            return false;
        }
        
        @Override
        public boolean modiflyEmployee(Employee employee) {
            Employee employee1 = repository.save(employee);
            if (employee != null) {
                return true;
            }
            return false;
        }
        
        @Override
        public Employee getEmployeeById(int id) {
            //Jpa的getOne方法,如果id不存在就会抛出异常,在进行操作是,应先确定其存在
            if (repository.existsById(id)) {
                return repository.getOne(id);
            }
            Employee employee = new Employee();
            employee.setName("no this employee");
            return null;
        }
        
        @Override
        public List<Employee> listAllEmployee() {
            return repository.findAll();
        }
    }

    web层Controller——>

    @RestController
    @RequestMapping("/provider/employee")
    public class EmployeeController {
        @Autowired
        private EmployeeService service;
    ​
        @PostMapping("/save")
        public boolean saveHandler(@RequestBody Employee employee) {
            return service.saveEmployee(employee);
        }
    ​
        @DeleteMapping("/del/{id}")
        public boolean removeEmployeeById(@PathVariable int id) {
            return service.removeEmployee(id);
        }
    ​
        @PostMapping("/update")
        public boolean updateHandler(@RequestBody Employee employee) {
            return service.modiflyEmployee(employee);
        }
    ​
        @GetMapping("/get/{id}")
        public Employee getOneById(@PathVariable int id) {
            return service.getEmployeeById(id);
        }
        
        @GetMapping("/list")
        public List<Employee> listAllEmployee() {
            return service.listAllEmployee();
        }
    }
    Spring Boot启动类——>
    @SpringBootApplication
    public class ProviderRun {
        public static void main(String[] args) {
            SpringApplication.run(ProviderRun.class, args);
        }
    }

    配置文件 application.yml——>

    server:
      port: 8081
    spring:
      jpa:
        database: mysql     #数据库类型为mysql
        generate-ddl: true  #在spring容器启动时,根据Bean自动创建数据表
        show-sql: true      #指定在控制台是否显示sql语句
        hibernate:
          ddl-auto: none    #指定应用重启时,不重新建表
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        url: jdbc:mysql://localhost:3306/mytest?serverTimezone=UTC
        username: root
        password: root
    ​
    logging:
      #设置日志输出格式
      pattern:
        console: level-%level %msg%n
      level: 
        root: info     #Spring Boot启动时的日志级别
        org.hibernage: info #hibernate的运行日志级别
        org.hibernate.type.descriptor.sql.BasicBinder: trace
        org.hibernate.hql.internal.ast.exec.BasicExecutor: trace
        com.ccl: debug

    这个时候,我们就可以启动这个这个服务了,然后通过测试工具,测试一下服务是否正常可访问,我已经测过了,所有这里不做过多验证。

    服务的消费者

    实体类bean:因为不接触到数据库,所以不做过多的JPA的注解——>

    /*提供有参无参,get set*/
    @Data
    public class Employee {
        private int id;
        private String name;
        private String dbase;
    }

    简化后的服务调用Controller——>

    @RestController
    @RequestMapping("/consumer/employee")
    public class ConsumerController {
        
        @Autowired
        private RestTemplate restTemplate;
    ​
        @PostMapping("/save")
        public boolean saveHandler(@RequestBody Employee employee) {
            String url = "http://localhost:8081//provider/employee/save";
            //第一个参数:服务提供着请求路径
            //第二个参数:我们要操作的对象
            //第三个参数:服务提供者的返回值类型
            return restTemplate.postForObject(url, employee, Boolean.class);
        }
    ​
        @DeleteMapping("/del/{id}")
        public void removeEmployeeById(@PathVariable int id) {
            String url = "http://localhost:8081/provider/employee/del" + id;
            //delete方法没有返回值,不做返回处理
            restTemplate.delete(url);
        }
    ​
        @PostMapping("/update")
        public void updateHandler(@RequestBody Employee employee) { 
            String url = "http://localhost:8081/provider/employee/update";
            restTemplate.put(url, employee);
        }
    ​
        @GetMapping("/get/{id}")
        public Employee getOneById(@PathVariable int id) {
            String url = "http://localhost:8081/provider/employee/get/" + id;
            return restTemplate.getForObject(url, Employee.class); 
        }
    ​
        @GetMapping("/list/ids")
        public List<Employee> listAllEmployee() {
            String url = "http://localhost:8081/provider/employee/list";
            return restTemplate.getForObject(url, List.class);
        }
    }

    Spring Boot启动类——>

    @SpringBootApplication
    public class ConsumerRun {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerRun.class, args);
        }
        
        @Bean
        public RestTemplate restTemplate(){
            //RestTemplate支持三种三种http客户端类型
            //HttpClient  、 OkHttp  、JDK原生的URLConnection(这个是默认的 )
            //默认就是不给参数,现在我们使用的是OkHttp
            return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
        }
    }

    配置文件application.yml——>

    server:
      port: 8082
    ​
    logging:
      #设置日志输出格式
      pattern:
        console: level-%level %msg%n

    测试工具:Postman

    扭开Postman测试工具,发起请求验证服务消费者是否通过调用服务提供者的服务,间接的操作数据库数据,完成服务的远程调用

      

    在获取一条数据试试

      

    完美。第一个远程服务的调用就这么完成了,你,Get到了吗?

    没Get到也没关系,因为上面这个东西,太原生了,上面的Demo存在明显的短板:

    • 比如消费服务时的访问路径、硬编码在逻辑代码中。后期发生变更不易维护

    • 其次万一服务的提供者宕机,服务的消费者也不知道

    • 然后就是服务的提供者的集群,混在均衡得自己实现

    SpringCloud的第一个组件:Eureka

    Eureka:中文意思"我发现了","我找到了",你们知道Zookeeper吗,就是Dubbo建议使用的注册中心那个Zookeeper,二者就是差不多的,但是Dubbo和Eureka侧重点不同,Eureka是牺牲了一致性,保证了可用性,而Zookeeper牺牲了可用性,保留了一致性,所谓的CAP的"三二原则",

    Eureka的作用这里也简单带过:

    • 【服务的注册、发现】:负责管理,纪录服务提供者的信息,服务调用者无需自己寻找服务的,而是把自己的需求告诉Eureka,Eureka就会吧符合你需求的服务告诉你,好比租房中介。

    • 【服务的监控】:服务的提供者和Eureka之间还通过"心跳",保持着联系,当某个服务提供方出现了问题,Eureka自会在指定的时间范围内将其剔除,就好比租房子,房东已经把房子租出去了,中介就会把这个房子排除在自己掌握的房源名单里

    这张图理解为一个房东,一个中介,一个打工仔:

    • 房东有房,把房子托给中介公司帮忙出租,房东和中介保持着联系(心跳),如果这个房子房东自己z住不想出租了或者房子漏水暂时不租了,中介第一时间就会知道,然后停止该房子的出租,而打工仔一个人孤苦伶仃的来到一个陌生的城市拼搏,他找到了中介,中介给了他一种表单,里面罗列了这个中介的所有的房源,看他需求什么,自己有的话就可以帮他联系上房东,通过这种方式,打工仔身在异乡但仍然感受到了家的温暖。

    Eureka:中介

    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                                                http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>cloud-demo</artifactId>
            <groupId>com.ccl.demo</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>00-eureke-server</artifactId><dependencyManagement>
            <dependencies>
                <!-- springCloud -->
                <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>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
                <version>2.0.1.RELEASE</version>
            </dependency>
        </dependencies>
    </project>

    application.yml——>

    server:
      port: 8080
    spring:
      application:
        name: Eureka-Server  #会在Eureka服务列表显示的应用名
    eureka:
      instance:
        hostname: localhost
      client:
        register-with-eureka: false #是否注册值的信息到Eureka,默认True
        fetch-registry: false   #是否拉取Eureka上的服务列表,当前是Server,不需要
        service-url:  # EurekaServer的地址,如果是集群,需要加上其它Server的地址。
          defaultZone: Http://${eureka.instance.hostname}:${server.port}/eureka

    启动类——>

    @SpringBootApplication
    @EnableEurekaServer
    public class EurekServerRun {
        public static void main(String[] args) {
            SpringApplication.run(EurekServerRun.class, args);
        }
    }

    服务的提供者:房东向中介注册房子

    我们上一个服务提供者,稍加改造:

    pom.xml—添加Eureka客户端依赖—>

    <!-- Eureka客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    aapplication.yml——>

    增加了application.name和Eureka相关的配置

    server:
      port: 8081
    spring:
      application:
        name: 02-provider
    eureka:
      client:
        service-url:  #Eureka的地址
          defaultZone: Http://localhost:8080/eureka
      instance:
        prefer-ip-address: true #当调用getHostname获取实例的hostname时,返回ip而不是host名称
        ip-address: 127.0.0.1 #指定自己的ip,不指定的话会自己寻找

    这里注意一下:

    • 不用指定register-with-eureka和fetch-registry,因为默认是true

    • fetch-registry: true #eureka注册中心配置 表明该项目是服务端,不用拉取服务 register-with-eureka: true #不用在eureka中注册自己

    启动类:

    @SpringBootApplication
    @EnableDiscoveryClient //开启Eureka客户端
    public class ProviderRun {
        public static void main(String[] args) {
            SpringApplication.run(ProviderRun.class, args);
        }
    }

    至于中间业务层和持久层,和上一个Demo一样,不做说明

    这个时候访问localhost:8080,应该就会发现相关的服务应该被注册上了,但我此时不做演示,一轮测试

    服务的消费者:打工仔向中介打听房子

    改造之前的服务消费者,这次我们要想Eureka索取在线服务列表,调用我们想调用的服务

    添加依赖:

    <!-- Eureka客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    配置application.yml:

    server:
      port: 8082
    spring:
      application:
        name: 02-consumer
    eureka:
      client:
        service-url: #Eureka的地址
          defaultZone: http://localhost:8080/eureka
      instance:
        prefer-ip-address: true #当其它服务获取地址时提供ip而不是hostname
        ip-address: 127.0.0.1 #指定自己的ip信息,不指定的话会自己寻找

    启动类:

    @SpringBootApplication
    @EnableDiscoveryClient //开启Eureka客户端
    public class ConsumerRun {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerRun.class, args);
        }
        
        @Bean
        public RestTemplate restTemplate(){
            //RestTemplate支持三种三种http客户端类型
            //HttpClient  、 OkHttp  、JDK原生的URLConnection(这个是默认的 )
            //默认就是不给参数,现在我们使用的是OkHttp
            return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
        }
    }

    Controller:

    @RestController
    @RequestMapping("/consumer/employee")
    public class ConsumerController {
        
        @Autowired
        private RestTemplate template;
        @Autowired
        private DiscoveryClient client;
        
        @GetMapping("/getAll")
        public List<Employee> allListEmployee(){
            //根据服务的名称获取服务的实列,一个服务可能有多个提供者,所以是List
            //你可以把这个步骤想象成在向Spring容器根据接口索要实列对象
            List<ServiceInstance> instances = client.getInstances("02-provider");
            for (ServiceInstance si : instances){
                String host = si.getHost();
                int port = si.getPort();
                System.out.println(host + ":" + port);
                //获取的ip和端口,动态的组装成url
                String url = "http://" + host + ":" + port + "/provider/employee/list";
                //发起请求,获得数据并返回
               List employees = this.template.getForObject(url, List.class);
               return employees;
            }
              return null;
        }

    Postman登场发表获奖感言

      

    我们通过Eureka获取到了指定应用名的服务的IP和Port,动态的组成了我们的请求Rest连接,成功的调用了服务的提供者的一个接口

    然后我们去看看我们的Eureka:

      

    暂时就这么点东西吧,当然Eureka还支持集群和负载均衡:

    还记得之前的故事吗?中介给了打工仔一个表单,在实际中也是这样的,Eureka将所有的服务列表全部给调用方,调用方自己匹配,负载均衡是调用方自己的事,而不是Eureka的事,他只是一个房子的搬运工,负载均衡也很简单,下面来看看: 在返回RestTemplate的方法上给一个注解 : @LoadBalanced,默认使用的轮询的方式,也可以指定为特定的负载均衡算法

      

    Eureka集群:多个中介

    说到集群,避免单点故障,提高吞吐量,我们就弄三个Eureka服务吧,因为都是本地为了区分,这里又得动hosts文件了:

    先说一下思路:三个Eureka

    1. 第一步分别修改 eureka.instance.hostname: eureka8080 / eureka8081 / eureka8082

    2. 第二步修改本地的映射文件,eureka8080 / eureka8081 / eureka8082 都指向192.0.0.1

    3. 第三步删除register-with-eureka=false和fetch-registry=false两个配置。这样就会把自己注册到注册中心了。

    4. 第四步将参与集群的机器全部罗列在service-url : defaultZon中,整体如下:

    5. 然后就是修改我们本地的映射文件,为了区分,不为其他的用途,如下

    然后这几个机器算是形成集群了,我们先启动访问测试一下:

    接下来就修改一下我们的服务注册者和服务消费者的注册和拉取请求连接即可:

    修改服务提供者的配置文件:

      

    修改服务消费者的配置文件:

      

    不慌,我的端口和前面的Eureka的端口碰撞了,这里重新修改了端口,在这里还得注意一下,不知道是不是使用负载均衡的原因,还是因为使用了集群的原因,我们的RestTemplate在发起请求的时候不能再拼接真实的地址了,会服务器异常,抛出异常:No instances available for 127.0.0.1

    只能用在Eureka中注册的服务名进行调用,如下:

      

    Postman测试工具启动:完美获得数据;

    结束语 = 补充干货

    但我以前的学习中的有些内容并没有讲解,我发现这个课程并没有讲,所以我做点补充吧!

      

    上面的很多属性经过这个Demo + 后面的注释,我相信大家都已经比较熟悉了,但是下面的部分属性,算作补充内容吧

    • 从服务的注册说起

    服务的提供者在服务启动时,就会检查属性中的是否将自己也想Eureka注册这个配置,默认为True,如果我们没有手动设置,那么就会向Eureka服务发起一个Rest请求,并带上自己的数据,Eureka收到这些数据时,就会将其保存到一个双层Map结构中,第一层Map的key就是服务名称,第二层Map的key就是服务实列id

    • 然后再说服务的续约,牵扯到上图的属性

    在我们的服务注册完成之后,通过心跳维持联系,证明自己还或者(这是服务的提供者定时向Eureka发),这个我们成为服务的续约,续约方式:"发起心跳"

    然后就有两个属性,可以被我们安排上:在开发上可以如我那样配置参数

    • lease-renewal-interval-in-seconds: 30 :每隔30秒一个心跳

    • lease-expiration-duration-in-seconds: 90 :90秒没有动静,认为服务提供者宕机了

    • 再然后就是 "实列id"

    看看这张图:

    UP(1) : 表示只有一个实列

    DESK...:port :实列的名称(instance-id)

    • 默认格式就是 "hostname" + "spring.application.name" + "server.port"

    • 可以改成为我笔记中的那样,简单明了且大方!

    • 这里得说明一下这个属性是区分统一服务不同实现的唯一标准,是不能重复的

    • 服务的提供者说完,我们来说服务的消费者

    当我们的服务消费者启动后,会检测eureka.client.fetch-registry=true,如果为true,就会去备份Eureka的服务列表到本地,并且可以通过registry-fetch-interval-seconds: 5 来设置多久更新一下备份数据,默认30 ,生产环境下不做修改,开发环境可以调小

    • 服务的提供者也说完了,最后我们就来说说Eureka吧

      • 失效剔除

      有时候,我没得服务提供方并不一定是宕机了,可能是一起其他的原因(网络延迟啥的)造成的服务一时没有正常工作,也就是没心跳且超贵最长时限,Eureka会将这些服务剔除出自己的服务列表

      属性:eureka.server.eviction-interval-timer-in-ms,默认60S 单位毫秒,

      • 自我保护

      这个横幅就是触发了Eureka的自我保护机制了,当一个服务为按时心跳续约时,Eureka会统计最近15分钟所有的服务的爽约比列,超过85%(生产环境下因为网络原因及其有可能造成这么局面),Eureka此时并不会将服务给剔除,而是将其保护起来,生产环境下作用还是很明显,起码不会因为网络原因导致大部分服务爽约,Eureka将全部服务剔除的局面出现

      但在开发中,我们一般将其关闭 :enable-self-preservation: false

    SpringCloud的另一个组件:OpenFeign

    大家可以回忆一下之前我们写的Demo,,没有回忆的话,我疏导回忆一下,最开始我们RestTemplate,当时直连消费者和提供者,将请求路径写死在代码中,而且负载均衡只有自己手写,RestTemplate只能给我们提供远程调用的功能,后来我们加入了Eureka,作为一个中间人,利用Eureka的服务的注册发现和监控和负载均衡,通过Eureka客户端获取指定服务名的ip或者应用名+端口,动态拼接成url,外加上RestTemplate一起完成远程的调用,但你有没有发现RestTemplate这个包装类有点小问题,而且这样搞起来很麻烦

    1. 服务提供者有返回数据,但经过RestTemplate相关api的CRUD的API没有返回值为void

    2. 再者,分模块开发,我们根本不知道服务的提供者的返回值是什么,不可能一个一个的问

    下面我们就要学习另一个组件取代RestTemplate,他就是OpenFeign

    优雅的简单介绍

    Feign的中文意思是伪装、装作的意思;OpenFeign是Feign的更新版本,是一种声明式REST客户端,使用起来听说更为便捷和优雅!Spring Boot1.X的时候就叫feign,我用的就是Feign;SpringCloud对Feign进行了增强,这个Feign也是那个租碟片公司研发的组件;

    快速上手

    首先是依赖问题,这里有点出入:

    首先是第一个依赖肯定是要加的,后面两个依赖是后来百度异常信息加上的,方可运行

    <!--openFeign的依赖 以下三个,不然抛出ClassNotFoundException-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
                <version>2.0.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>18.0</version>
            </dependency>
            <dependency>
                <groupId>com.netflix.archaius</groupId>
                <artifactId>archaius-core</artifactId>
                <version>0.7.6</version>
            </dependency>

    服务的提供者:无需改动

    服务的消费者:

    定义service接口,,这是Feign的核心所在,下面详细说明

    @FeignClient("03-provider") //属性为服务提供者的应用名
    @RequestMapping("/provider/employee")
    public interface EmployeeService {
        @PostMapping("/save")
        boolean saveEmployee(Employee employee);
        @DeleteMapping("/del/{id}"boolean removeEmployee(@PathVariable("id") int id);
        @PostMapping("/update")
        boolean modiflyEmployee(Employee employee);
        @GetMapping("/get/{id}")
        Employee getEmployeeById(@PathVariable("id")int id);
        @GetMapping("/list")
        List<Employee> listAllEmployee();
    }
    • 首先这是一个接口,在服务的消费方,你可以把他当作Service层(偷懒)也可以当成Dao层

    • 首先整体上来讲,Feign会通过动态代理帮我们生成实现类;

    • 其次开局第一个注解@FeignClient,生命这是一个Feign客户端,同时通过value指定了服务提供者应用名

    • 最后接口中定义的方法,方法是来自于服务提供者的service接口中的方法,但是方法上的注解确实来自服务提供者Controller上的注解,完全采用SpringMVC的注释,Feign会根据注解帮我们生成URL,并访问相应的服务接口

    然后你可以在Controller层或者service层直接@Autowired这个接口,直接调用接口中的方法即可实现远程调用

    然后还得在启动类上添加注解如下:

    @SpringBootApplication
    @EnableDiscoveryClient //开启Eureka客户端
    @EnableFeignClients(basePackages = "com.ccl.test.service") //开启Feign,并指定Service所在的包
    public class ConsumerRun {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerRun.class, args);
        }
    }

    此外,Feign中还集成了Ribbon负载均衡,所以这里我们直接抛弃了RestTemplate,接下来把项目跑起来,通过我们的服务消费者远程调用服务提供者的api完成这次远程调用;说到这里,我们就必须得明白Ribbo了,下面我们就来学习一下

    SpringCloud的另一组件:Ribbon

    优雅而不失风度的简单介绍

    • Ribbon还是那个当初租碟片营生起家的NetFlix公司研发发布的,并被SpringCloud集成到了项目中,当我们为Ribbon配置服务提供者的地址列表后,Ribbon就可以根据多种之一的负载均衡算法自动的去分配服务消费者的请求;

    • 由于我们已经学习过了Feign,所以RestTemplate的负载均衡我记得之前的笔记中是有的,在返回RestTemplate到Spring容器的方法上加上一个@LoadBalanced注解即可实现,只是现在被我证实了,当我们使用Ribbon负载均衡后,我们不能再通过拼接ip+port的方法发起调用,只能通过应用名的方式发起远程调用,因为这是Ribbon根据应用名相同采取负载均衡的前提;

    • 下面我们来说说我们后面常用的OpenFeign的Ribbon负载均衡,Feign本身也是集成了Ribbon的依赖和自动配置的,在这里我们就要单独的配置Ribbon了,而不是简单的加一个注解

      03-provider: ribbon: ConnectTimeout: 250 # 连接超时时间(ms) ReadTimeout: 1000 # 通信超时时间(ms) OkToRetryOnAllOperations: true # 是否对所有操作重试 MaxAutoRetriesNextServer: 1 # 同一服务不同实例的重试次数 MaxAutoRetries: 1 # 同一实例的重试次数

      #这个配置是为某个03-provider这个服务配置的局部坏均衡,若要全局,不需要指定服务名,直接ribbon.XXX实现全局配置

    Ribbon的负载均衡算法之IRule接口

    Ribbon提供了点多钟轮询算法,常见负载均衡算法比如默认的轮询,其他的随机、响应时间加权算法等;

    想要修改Ribbon的负载均衡算法,就必须得知道下面这个接口

    IRule接口

    • Ribbon的负载均衡算法需要实现IRule接口,该接口中和核心方法就是choose()方法,对服务提供者的选择方式就是在该方法中体现的,该方法就是在所有可用的方法集合中选择一个可用的服务

    7个均衡算法

    1. RoundRobbinRule:轮询策略

    2. BestAvailableRule:选择并发量最小的服务策略

    3. AvailabilityFilteringRule:过滤掉不可用的provider,在剩余的provider中采用轮询策略

    4. ZoneAvoidanceRule:复合判断provider所在区域的性能及可用性选择服务器

    5. RandomRule:随机策略

    6. RetryRule:先按照轮询的策略选择服务。若获取失败则在指定的时间内重试,默认500毫秒

    7. WeightedResponseTimeRule:权重响应时间策略,根据每个provider的响应时间计算权重,响应时间越快,被选中的几率就越高,刚启动时采用轮询策略,后面就转换为根据选择选择

    更换负载均衡算法也很简单,在我们的启动类下将其被我spring容器所管理即可

    当然也是使用配置方式配置指定的负载均衡策略:

     03-consumer:
      ribbon:
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

    格式就是 :{服务名称}.ribbon.NFLoadBalancerRuleClassName`,值就是IRule的实现类。

    当然这是在他规定的集中负载均衡算法中选取,我们也可以自定义算法,但我觉得没有必要,你说是不是?官方都给了7中算法,我们还去自定义算法,是不是不太合适?如果非要使用自定义算法的话,实现IRule接口,重写方法,将其给Spring容器管理。

    SpringCloud的另一组件:OpenFeign + Hystrix

      

    在学习这个组件之前,有两个专业性名词需要我们在一起学习一下

    服务熔断/降级理解

    • 服务熔断

      • 服务雪崩:是一种因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程。

      • 雪崩效应:服务提供者因为不可用或者延迟高在成的服务调用者的请求线程,阻塞的请求会占用系统的固有线程数、IO等资源,当这样的被阻塞的线程越来越多的时候,系统瓶颈造成业务系统炎黄崩溃,这种现象成为雪崩效应

      • 熔断机制:熔断机制是服务雪崩的一种有效解决方案,当服务消费者请求的服务提供者因为宕机或者网络延迟高等原因造车过暂时不能提供服务时,采用熔断机制,当我们的请求在设定的最常等待响应阀值最大时仍然没有得到服务提供者的响应的时候,系统将通过断路器直接将吃请求链路断开,这种解决方案称为熔断机制

    • 服务降级

      • 理解了上面所说的服务熔断相关的知识,想想在服务熔断发生时,该请求线程仍然占用着系统的资源,为了解决这个问题,在编写消费者[重点:消费者]端代码时就设置了预案,当服务熔断发生时,直接响应有服务消费者自己给出的一种默认的,临时的处理方案,再次注意"这是由服务的消费者提供的",服务的质量就相对降级了,这就是服务降级,当然服务降级可以发生在系统自动因为服务提供者断供造成的服务熔断,也可运用在为了保证核心业务正常运行,将一些不重要的服务暂时停用,不重要的服务的响应都由消费者给出,将更多的系统资源用作去支撑核心服务的正常运行,比如双11期间,收货地址服务全部采用默认,不能再修改,就是收货地址服务停了,把更多的系统资源用作去支撑购物车、下单、支付等服务去了。

      • 我们再简单归纳一下:简单来说就是服务提供者断供了,其一为了保证系统可以正常运行,其二为了增加用户的体验,由服务的消费者调用自己的方法作为返回,暂时给用户响应结果的一种解决方案;

    Hystrix的中文意思是:豪猪,在我们的应用中Hystrix的作用就是充当断路器

      

    关于单独的Hystrix就不做过多的阐述,因为他虽然可以单独使用,但在我们SpringCloud中,Feign默认也有对Hystrix的集成,只不过默认情况下是关闭的,需要我们手动开启

      

    Demo工程快速搭建

    关于Fallback的配置这就得我们自己手动配置了,如下

    首先服务消费者引入相关依赖:

            <!--Hystrix-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
                <version>2.0.1.RELEASE</version>
            </dependency>

    application.yml的配置如下:Feign

    feign: #feign对hystrix的支持开启,注意这些属性idea不会给出提示
      hystrix:
        enabled: true
    hystrix: #服务响应慢,无响应的熔断保护机制  
      command:
        default:
          execution:
            isolation:
              thread:
                timeoutInMillisecond: 5000 # 熔断超时时长:默认50000ms

    服务消费者编写回滚函数

    /**
     * 定义一个类,实现FallbackFactory,并将Feign客户端传入
     * 通过下面create方法内部使用内部类方式做一个Feign客户端的实现
     * 当我们的某个方法服务不可用时,就调用我们内部类中对应的方法
     */
    @Component
    public class EmployeeServiceFallback implements FallbackFactory<EmployeeService> {
    ​
        @Override
        public EmployeeService create(Throwable throwable) {
        return new EmployeeService(){ //待会儿我们就以这个方法为列进行测试 @Override public Employee getEmployeeById(int id) { Employee employee = new Employee(); employee.setName("No this Employee"); employee.setDbase("No this Employee"); return employee; } @Override public boolean saveEmployee(Employee employee) { System.out.println("服务降级,saveEmployee服务不可用,请稍后再试"); return false; } @Override public boolean removeEmployee(int id) { System.out.println("服务降级,removeEmployee服务不可用,请稍后再试"); return false; } @Override public boolean modiflyEmployee(Employee employee) { System.out.println("服务降级,modiflyEmployee服务不可用,请稍后再试"); return false; } @Override public List<Employee> listAllEmployee() { System.out.println("服务降级,listAllEmployee服务不可用,请稍后再试"); return null; } }; } }

    FeignClient如下:

    @Service
    //fei客户端 第一个参数为服务提供者的应用名 第二个为回滚的指定实现所在
    @FeignClient(value="03-provider",fallbackFactory = EmployeeServiceFallback.class)
    @RequestMapping("/provider/employee")
    public interface EmployeeService {
        @PostMapping("/save")
        boolean saveEmployee(Employee employee);
        @DeleteMapping("/del/{id}")
        boolean removeEmployee(@PathVariable("id") int id);
        @PostMapping("/update")
        boolean modiflyEmployee(Employee employee);
        @GetMapping("/get/{id}")
        Employee getEmployeeById(@PathVariable("id")int id);
        @GetMapping("/list")
        List<Employee> listAllEmployee();
    }

    然后就是启动类上开启服务降级

    @SpringBootApplication
    @EnableDiscoveryClient //开启Eureka客户端
    @EnableFeignClients(basePackages = "com.ccl.test.service") //开启Feign,并指定Service所在的包
    @EnableCircuitBreaker //开启Hystrix的服务降级
    public class ConsumerRun {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerRun.class, args);
        }
    }

    好了服务的消费者方就已经准备好了,就差服务方因为网络延迟等原因造成没有响应了,我们在服务的提供方制造一个异常,我们将异常写进我们待会服务调用会使用到的方法中,造成服务不可用

    @Override
        public Employee getEmployeeById(int id) {
            if (repository.existsById(id)) {
                //手动创造一个异常,待会儿调用就会抛出异常造成服务不可用
                int flag = 1 / 0;
                return repository.getOne(id);
            }
            Employee employee = new Employee();
            employee.setName("no this employee");
            return null;
        }

    Postman夺台发表感言

    双双启动项目和Eureka,进行测试

      

    可以发现,因为服务的提供者因为i我们的手动异常造成了服务的不可用,启动了服务的降级,调用了服务消费者本地的的相对业务逻辑,并返回了数据:

      

    SpringCloud的另一组件:网关Zuul

      

    经过前面的学习,目前我们对于SpringCloud基本技术栈已经快一半了,我们使用Eureka实现服务的注册与发现,服务之间通过Feignj进行调用,并通过Ribbon实现负载均衡,在服务的过程中,可能服务的提供者会断供,我们通过Hystrix的熔断机制实现了服务的降级和故障的蔓延,下面我们将学习网关Zuul和外部化配置管理的springCloud config,学完这两个SpringCloud基本上的技术栈就算入门了,可以正常做开发了。

    • Zuu:服务网关,一个微服务中不可获取的组件,通过Zuul网关统一向外提供Rest API,服务网关除了具备服务路由、负载均衡的功能之外,他还得具备权限控制等功能,在SpringCloud中,Zuul就是处于对外访问最前端的地方:

    Zuul加入后的架构

    • 不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。

    快速搭建Zuul

    我们就不一步一步来了,直接在怼最终版的时候再补充,首先依赖:

    这里说一下,Zuul是必须的,然后Eureka的依赖是为了Zuul去Eureka拉取服务所以这里就需要这连个依赖

       <dependencies>
            <!--Zuul-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
                <version>2.0.1.RELEASE</version>
            </dependency>
            <!--Eureka-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                <version>2.0.1.RELEASE</version>
            </dependency>
        </dependencies>

    启动类:

    @SpringBootApplication
    @EnableZuulProxy //开启Zuul的网关功能
    @EnableDiscoveryClient  //开启Eureka的客户端发现功能
    public class zuulRun {
        public static void main(String[]args) {
            SpringApplication.run(zuulRun.class,args);
        }
    }

    然后就是配置文件application.yml

    server:
      port: 9000
    spring:
      application:
        name: zuul-gateway
    eureka:
      client:
        registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
        service-url: #Eureka的地址
          defaultZone: Http://localhost:8000/eureka
      instance:
        prefer-ip-address: true #当其它服务获取地址时提供ip而不是hostname
        ip-address: 127.0.0.1 #指定自己的ip信息,不指定的话会自己寻找
    ​
    zuul:
      routes:
        03-provider:               #这里的可以随便写
          serviceId: 03-provider   #指定服务名称
          path: /haha/**           #这里是映射路径

    前面的配置我相信大家心里都知道的七七八八了吧,我们就说说zuul下的配置

    按照进化版本,我这里也一一罗列出来

    • 原始版本,了解即可,无需Eureka

      zuul:
        routes:
         03-provider:                  # 这里是路由id,随意
            url: http://127.0.0.1:8090  # 映射路径对应的实际url地址
            path: /03-provider/**      # 这里是映射路径

      讲解:我们将复合path规则的一切请求都代理到url参数指定的地址

    • 初级进化,了解即可,加持Eureka

      zuul:
        routes:
          03-provider:               #这里的可以随便写
            serviceId: 03-provider   #指定服务名称
            path: /haha/**           #这里是映射路径

      讲解:因为加持了Eureka,我们可以去Eureka去获取服务的地址信息,通过服务名来访问

      到了这一步以为是通过服务名来获取服务的,所以集成了Ribon的负载均衡功能

      因为我们的Zuul的端口为9000,:http://localhost:9000/haha/provider/employee/get/1

      网关地址 + 映射路径 + Controller的@RequestMapping的映射规则,完成访问

    • 究级进化,掌握

      zuul:
        routes:
          03-PROVIDER : /03-provider/**

      解释:这个就需要掌握了,后面我们常用的的

      默认情况下,我们的路由名和服务名往往是一致的,因此Zuul提供了一套新的规则,就如上面那样

      服务名 : 映射路径 ;访问路径为:http://localhost:9000/03-provider/provider/employee/get/1

      因为这个这个规则有个特点,就是映射路径就是服务名本身,所有即使我们不做配置,也是能访问的,但有时候还是要配,要配一些其他的属性

    在究级进化的基础上,我们还有一些其他的属性可以配置

     再添加两个常用的属性:

      sensitiveHeaders:敏感头设置,默认Zuul是将cookie拦截再黑名单中的,这样设置为空,表示不过滤

      ignoreHeaders:可以设置过滤的头信息,这里我们设置为空,表示不过滤任何头

    Zuul的过滤器

    Zuul最为网关使他的一个功能之一,我们想实现请求的鉴权,就是通过Zuul提供的过滤器来实现的,下面我们来认识认识一下

    • ZuulFilter:过滤器的顶级父类

      @Component
      public class loginFilter extends ZuulFilter {
          @Override
          public String filterType() {
              //返回过滤器的类型[pre、routing、post、error]
              //请求在被路由之前、之时调用,在routing之后error之前,处理请求发生错误时
              return null;
          }
          @Override
          public int filterOrder() {
              //返回int值表示该过滤器的的优先级,越小越高
              return 0;
          }
          @Override
          public boolean shouldFilter() {
              //是否启动该过滤器
              return false;
          }
          @Override
          public Object run() throws ZuulException {
              //过滤器的具体业务逻辑
              return null;
          }
      }
      • 下面我们再看一官网的提供的请求生命周期图,表现了一个请求在各个过滤器的执行顺序

        

      • 正常流程:

        • 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。

      • 异常流程:

        • 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。

        • 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。

        • 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。

      • 使用场景

      • 请求鉴权:一般放在路由之前,如果发现没有权限,可以拦截+转发,比如淘宝没登录查看购物车,拦截转发到登录页面

      • 异常处理:一般会放在erroe类型和post类型过滤器中结合来处理

      • 服务时长统计:post的现在时 - pre的现在时

    上面我们自定义了过滤器,下面我们就赖模拟一个登录的校验,如果请求中有access-token参数,我们就放行,如果没有我们就拦截不做其他表示

    @Component
    public class loginFilter extends ZuulFilter {
        @Override
        public String filterType() {
            //返回过滤器的类型[pre、routing、post、error]
            //请求在被路由之前、之时调用,在routing之后error之前,处理请求发生错误时
            return "pre";
        }
        @Override
        public int filterOrder() {
            //返回int值表示该过滤器的的优先级,越小越高
            return 1;
        }
        @Override
        public boolean shouldFilter() {
            //是否启动该过滤器
            return true;
        }
        @Override
        public Object run() throws ZuulException {
            //过滤器的具体业务逻辑
            RequestContext context = RequestContext.getCurrentContext();
            String token = context.getRequest().getParameter("login-token");
            if (token == null || "".equals(token.trim())){
                context.setSendZuulResponse(false);
                //返回401状态码,也可以重定向到某个页面
                context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
                System.out.println("拦截到一个请求,请求处理");
            }
            //校验通过,可以把用户信息啥的方法放到LocalThread等操作
            return null;
        }
    }

    准备就绪,开始访问,没有token时 和有Token访问时:http://localhost:9000/03-provider/provider/employee/get/1

    我们把login-token带上,进行测试:http://localhost:9000/03-provider/provider/employee/get/1?login-token=123

    负载均衡和熔断的支持

    Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。不配置的话都走的默认值,不妥:

      

     

  • 相关阅读:
    3.0.35 platform 设备资源和数据
    老子《道德经》第六十三章
    linux spi 主机与外设分离思想
    3.0.35 platform 总线、设备与驱动
    Linux 内核SPI子系统架构
    C 显示一个字符的编码值
    JAVA高级特性--内部类
    Java面向对象----接口概念
    Java面向对象----抽象类
    Java面向对象----多态概念,对象上下转型
  • 原文地址:https://www.cnblogs.com/msi-chen/p/10508829.html
Copyright © 2020-2023  润新知