openfeign的实际应用

1、openfeign简介

在谈openfeign之前,我们先说下另一个框架:feign。feign是一个声明式的web服务客户端框架,它提供了一些注解,只要将它们加到某个接口和接口内部的方法上,那就可以很方便地直接用这个接口来开发http客户端代码。再细化来说,就是能让我们写更少的代码来访问服务端的http接口。拿我们熟知的jdk自带的HttpURLConnection类来说,我们要是使用它来访问http接口,加上解析http接口响应的数据,写个十几二十几行代码都是很正常的,就算是用apache的httpClient框架,也是如此。而通过使用feign,我们只需要调用接口的某个方法,就能完成http接口调用。

我们再来说下openfeign,它是Spring Cloud官方提供的,是在feign框架的基础上加了一些对spring mvc注解的支持,同时也支持我们开发spring web应用时所使用的一系列http消息转换器(当然这些基本都是spring框架内置的功能)。而且作为Spring Cloud官方的产品,它天然支持使用eureka、Spring Cloud CircuitBreaker和Spring Cloud LoadBalancer等组件,能使feign接口具备负载均衡的能力,就像咱们之前聊的restTemplate那样。

当然,feign和openfeign都是用来开发客户端代码的,意思就是这部分代码只能给客户端用,客户端通过这些代码来访问相应的服务端接口,而且无论是使用feign,还是使用openfeign,这部分代码基本都是要由服务端来完成开发,然后提供给客户端使用。就像我们开发dubbo服务一样,会提供相应的包含服务接口的jar包给客户端使用(后续会写文章来聊聊dubbo相关的知识)。

2、openfeign实战

和以前一样,针对技术讲解,我们会先聊聊技术的实际应用,然后再说原理。毕竟我们先会用,才更容易去理解它的原理。那么本部分我们来说下openfeign在实际工程中的应用。

2.1、创建openfeign工程

通过idea工具创建一个名为openfeign的Maven工程,工程的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>
  <groupId>com.xk</groupId>
  <artifactId>openfeign</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>

  <name>openfeign</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>
  <modules>
    <module>springcloud-openfeign-provider</module>
    <module>springcloud-openfeign-consumer</module>
  </modules>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
    
  
    <dependencyManagement>
        <!--版本关系 https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2021.0.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.0.4.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.7.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

然后我们再在该工程下面创建两个子模块:

模块 备注
springcloud-openfeign-provider 服务提供者,采用springboot开发,用来提供http接口,供消费者调用
springcloud-openfeign-consumer 服务消费者,采用springboot开发,用来通过openfeign调用服务提供者的http接口

2.2、创建工程子模块

2.2.1、创建springcloud-openfeign-provider模块

provider的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>com.xk</groupId>
        <artifactId>springcloud-openfeign-provider</artifactId>
        <version>1.0</version>
    </parent>

    <artifactId>openfeign-provider-api</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

        <!--lombok包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
    </dependencies>

</project>

我们在该模块下面创建以下三个子模块:

模块 备注
openfeign-provider-api api模块,提供一些公共的dto类、util类和过滤器供client模块和web模块使用
openfeign-provider-client client模块,需要依赖api模块,通过openfeign封装web模块提供的http接口,然后供客户端调用
openfeign-provider-web web模块,需要依赖api模块,是个完整的springboot web应用,对外提供http服务

下面我们分开讲解各个子模块的写法。

2.2.1.1、openfeign-provider-api

api模块作为一个公共的模块,主要提供一些dto类、util工具类和过滤器,供client模块和web模块使用。

openfeign的实际应用
image-20230715151449950

api模块的主要类定义如下:

(1)OrderDTO类

package com.xk.openfeign.springcloud.provider.api.dto;

public class OrderDTO {
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

(2)TokenFilter类

TokenFilter主要是用来获取请求头中的token,然后将token设置到ThreadLocal对象中,方便后面直接从ThreadLocal中获取到token。ThreadLocal对象是和当前处理任务的线程绑定的,该线程可以在任何代码位置获取到和本线程相关的ThreadLocal对象中存储的信息,而不必将相关信息通过方法参数一步一步地往下传递。

package com.xk.openfeign.springcloud.provider.api.filter;

import com.xk.openfeign.springcloud.provider.api.util.WebThreadLocalUtil;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * token过滤器
 * 用于往线程本地变量工具类中设置token
 * 同时也可以借助请求头是否存在token来完成对用户是否登录的校验
 * @author xk
 * @date 2023-07-07 8:16
 */

public class TokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        //这里假设用户登录后,前端将用户token放到了请求头中
        String token = request.getHeader("token");
        //如果token为空,可以做相应的处理
        if(!StringUtils.hasText(token)){

        }

        //为了实验,此处暂时不考虑token为空
        WebThreadLocalUtil.setToken(token);
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

(3)WebThreadLocalUtil类

package com.xk.openfeign.springcloud.provider.api.util;

import java.util.HashMap;
import java.util.Map;

/**
 * 本地线程工具类
 * @author xk
 * @date 2023-07-07 8:14
 */

public class WebThreadLocalUtil {

    private static final String TOKEN = "token";

    private static final String USER = "user";

    protected static ThreadLocal<Map<String,Object>> localMap = new ThreadLocal<Map<String, Object>>(){
        @Override
        protected Map<String, Object> initialValue() {
            return new HashMap<>();
        }
    };

    /**
     * 根据key获取对应的值对象
     * @param key key
     * @return
     */

    public static Object getValue(String key) {
        return localMap.get().get(key);
    }

    /**
     * 设置键值对
     * @param key key
     * @param value value
     */

    public static void setValue(String key,String value){
        localMap.get().put(key,value);
    }

    /**
     * 获取当前登录用户的token
     * @return
     */

    public static String getToken() {
        return (String) localMap.get().get(TOKEN);
    }

    /**
     * 设置token
     * @param value token值
     * @return
     */

    public static void setToken(String value) {
        Map<String, Object> objectMap = localMap.get();
        objectMap.put(TOKEN,value);
    }

}

2.2.1.2、openfeign-provider-web模块

web模块依赖api模块,它就是一个普通的采用springboot开发的web应用,它本身和openfeign没什么关系,也不需要依赖openfeign相关的jar包,所以我们就像开发正常的web应用一样,来进行本模块的开发。另外,web模块的server.context-path被设置为/。

openfeign的实际应用
image-20230715151528601

web模块的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>com.xk</groupId>
        <artifactId>springcloud-openfeign-provider</artifactId>
        <version>1.0</version>
    </parent>

    <artifactId>openfeign-provider-web</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.xk</groupId>
            <artifactId>openfeign-provider-api</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <!--mybatis启动器-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <!--lombok包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>


    </dependencies>

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

</project>

可以看到,pom文件中依赖了openfeign-provider-api模块,并没有别的特别的东西。因为这次我们只关心http接口层的调用,所以我们就只看下controller层,关于mybatis的使用,这里就先不介绍了。

(1)OrderController类

package com.xk.openfeign.springcloud.provider.core.controller;

import com.xk.openfeign.springcloud.provider.api.dto.OrderDTO;
import com.xk.openfeign.springcloud.provider.core.entity.Order;
import com.xk.openfeign.springcloud.provider.core.service.OrderService;
import org.springframework.beans.BeanUtils;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author xk
 * @since 2023.04.24 14:25
 */

@RequestMapping("/order")
@RestController
public class OrderController{

    @Autowired
    private OrderService orderService;

    @GetMapping("{id}")
    public OrderDTO findById(@PathVariable Long id){
        Order order = orderService.findById(id);
        OrderDTO orderDTO = new OrderDTO();
        BeanUtils.copyProperties(order,orderDTO);
        return orderDTO;
    }
}

OrderController类就提供了一个简单的http查询接口,根据id从数据库查询数据。这里我们也可以简化一下,直接创建一个OrderDTO对象。

web模块我们就先说这块,接下来就说下咱们的重点,如何用openfeign开发client模块,并且能通过client模块直接访问到上面的http接口。

2.2.1.3、openfeign-provider-client模块

client模块是供客户端(消费者)使用的,它依赖api模块,并且将web模块的http接口进行封装,供客户端直接使用。

openfeign的实际应用
image-20230715151602179

其中client模块的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>com.xk</groupId>
        <artifactId>springcloud-openfeign-provider</artifactId>
        <version>1.0</version>
    </parent>

    <artifactId>openfeign-provider-client</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <dependency>
            <groupId>com.xk</groupId>
            <artifactId>openfeign-provider-api</artifactId>
            <version>1.0</version>
        </dependency>

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

        <!--lombok包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

    </dependencies>

</project>

pom文件中很重要的一点就是引入了spring-cloud-starter-openfeign包,版本号是3.1.3版本。

注意:web模块并不会依赖本模块。

接下来我们来看下client模块要提供哪些东西。

(1)TokenRequestInterceptor类

package com.xk.openfeign.springcloud.provider.client.interceptor;

import com.xk.openfeign.springcloud.provider.api.util.WebThreadLocalUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;

/**
 * token请求拦截器,用于在请求头放置token,来满足目标系统的校验
 * @author xk
 * @date 2023.07.07 09:06
 */

public class TokenRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("token", WebThreadLocalUtil.getToken());
    }
}

TokenRequestInterceptor是个拦截器类,它实现了feign包的RequestInterceptor接口,这个接口主要就是在发起请求前对请求的信息做一下预处理。我们此处使用这个,是为了在请求头中写入token参数,服务提供者可以根据请求头中的token来判断客户端是否有权限访问目标http接口。当然此处我们只是演示,而且是假定服务提供者就是通过获取请求头中的token字段的值来获取用户信息。

(2)OrderClient接口

package com.xk.openfeign.springcloud.provider.client;

import com.xk.openfeign.springcloud.provider.api.dto.OrderDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 客户端接口
 * url的值可以写死,比如:http://127.0.0.1:8080
 * 为了使用的灵活性,此处已变量形式注入
 */

@FeignClient(name="order",url="${app.order.url}")
public interface OrderClient{

    @GetMapping("/order/{id}")
    @ResponseBody
    OrderDTO findById(@PathVariable Long id);
}

OrderClient接口就是消费者要真正直接用到的,它内部的findById方法上面加了我们很熟悉的spring mvc注解。然后接口上面写了@FeignClient注解,openfeign包会对加了@FeignClient注解的接口进行解析,其中的url属性的值我们就以外部配置的方式来提供,当然配置项的名称已经由我们固定了,就是app.order.url,消费者在自己的配置文件中(也可以是启动参数)就需要声明这么一个配置项。

前面说过,openfeign是可以结合注册中心一起使用的,也就是可以通过提供服务的名称而不是url来完成对目标服务的访问。但是出于快速入门的需要,而且考虑到一些简单的服务可能并不需要依赖注册中心,所以本篇我们就先讲解openfeign直接通过目标服务的url进行调用的方式。

值得注意的是:当前3.1.3版本的openfeign不支持在标记@FeignClient注解的接口上使用@RequestMapping注解,及时我们用了,也不会生效。同时,@FeignClient的name属性也必须赋值,不可以为空。

(3)ClientAutoConfiguration配置类

package com.xk.openfeign.springcloud.provider.client;

import com.xk.openfeign.springcloud.provider.api.filter.TokenFilter;
import com.xk.openfeign.springcloud.provider.client.interceptor.TokenRequestInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Collections;

/**
 * feign客户端配置
 */

@EnableFeignClients
@Configuration
public class ClientAutoConfiguration {

    @Bean
    public TokenRequestInterceptor tokenRequestInterceptor(){
        return new TokenRequestInterceptor();
    }

    @Bean
    public FilterRegistrationBean<TokenFilter> tokenFilter() {
        FilterRegistrationBean<TokenFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new TokenFilter());
        bean.setUrlPatterns(Collections.singletonList("/*"));
        return bean;
    }
}

该配置类创建了基于feign的请求拦截器对象,以及过滤器对象。另外配置类上面加了一个很重要的注解@EnableFeignClients。

(4)resources目录下的META-INF/spring.factories文件

在client模块的resources资源目录下,创建META-INF目录,并在META-INF目录下创建spring.factories文件,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xk.openfeign.springcloud.provider.client.ClientAutoConfiguration

当消费者(客户端)引入openfeign-provider-client模块后,ClientAutoConfiguration配置类就会生效,同时配置类中的bean也会生效,而且openfeign接口也会被解析,并将生成的代理对象注入到消费者的spring容器中,方便消费者直接使用。

2.2.1.4、小结

到此,我们完成了openfeign服务提供者模块的编写,接下来就需要将client模块和api模块安装到本地仓库,方便消费者模块引用。当然,实际应用中,我们会把这两个模块打包部署到内网的maven私服,也方便团队人员使用。

2.2.2、创建springcloud-openfeign-consumer模块

本模块是个独立的springboot应用,主要用来演示如何调用其他服务的http接口,因此代码层面会简化,只写controller层。

openfeign的实际应用
image-20230715151630018

模块的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>com.xk</groupId>
        <artifactId>openfeign</artifactId>
        <version>1.0</version>
    </parent>

    <artifactId>springcloud-openfeign-consumer</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.xk</groupId>
            <artifactId>openfeign-provider-client</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <!--lombok包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>

    </dependencies>

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

</project>

pom文件中我们是直接引用了openfeign-provider-client模块,引入之后,也会自动将openfeign相关的包导入到项目中。

2.2.2.1、使用openfeign接口进行调用

我们来看下怎么使用openfeign-provider-client模块,来完成对目标服务的调用。

(1)代码

我们新建一个ConsumerController类,用于暴露http接口,用于自测。

内容如下:

package com.xk.openfeign.springcloud.consumer.core.controller;

import com.xk.openfeign.springcloud.provider.api.dto.OrderDTO;
import com.xk.openfeign.springcloud.provider.client.OrderClient;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author xk
 * @since 2023.07.07 09:01
 */

@RequestMapping("/consumer")
@RestController
public class ConsumerController {

    @Autowired
    private OrderClient orderClient;

    @GetMapping("{id}")
    public OrderDTO findById(@PathVariable Long id){
        return orderClient.findById(id);
    }
}

里面的内容很简单,直接将OrderClient对象注入到当前的Controller中,然后就像调用本地方法一样,完成对目标服务的调用,客户端在调用的时候,写法就是这么超级简单。

(2)配置文件

我们之前在写openfeign-provider-client模块的时候,提到过OrderClient要依赖外部的配置项,为了使服务正常使用,我们还需要在消费者(客户端)配置文件中写入下列信息:

yml格式:

app:
  order:
    #order服务的访问地址前缀
    url: http://localhost:8080

或properties格式:

app.order.url=http://localhost:8080

这里的url的值就是openfeign-provider-web服务所在服务器的ip和端口号。到这里,我们的consumer模块就能正常的通过openfeign接口访问服务提供者的http接口了。

2.3、小结

以前我们开发了一个http接口,会告诉别人具体的http接口地址,供别人调用,一旦对方写错了url地址,就会导致调用出错。而openfeign的出现,能大大简化这个调用的过程。别人不需要再记住我们的接口地址是哪个,只需要给别人一个jar包,然后告诉他调用哪个接口中的哪个方法就可以了。当然,我们自己因为要写只供别人使用的openfeign包,工作量就会稍微多点。但是,换一种角度想想,如果我们大家都这么做,那么彼此间的服务调用就都会更加地方便~

觉得有收获的朋友,可以点击关注我,这样方便接收我后续的文章,多谢支持~


原文始发于微信公众号(IT人的天地):openfeign的实际应用

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/186525.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!