<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Spring Boot+gRPC構(gòu)建微服務(wù)并部署到Istio(詳細(xì)教程)

          共 35149字,需瀏覽 71分鐘

           ·

          2022-06-17 18:08

          點擊上方[全棧開發(fā)者社區(qū)]右上角[...][設(shè)為星標(biāo)?

          點擊領(lǐng)取全棧資料全棧資料

          作為Service Mesh和云原生技術(shù)的忠實擁護(hù)者,我卻一直沒有開發(fā)過Service Mesh的應(yīng)用。正好最近受夠了Spring Cloud的“折磨”,對Kubernetes也可以熟練使用了,而且網(wǎng)上幾乎沒有Spring Boot微服務(wù)部署到Istio的案例,我就開始考慮用Spring Boot寫個微服務(wù)的Demo并且部署到Istio。項目本身不復(fù)雜,就是發(fā)送一個字符串并且返回一個字符串的最簡單的Demo。

          題外話:我本來是想用Spring MVC寫的——因為周圍有的同學(xué)不相信Spring MVC也可以開發(fā)微服務(wù),但是Spring MVC的各種配置和依賴問題把我整的想吐,為了少掉幾根頭發(fā),還是用了方便好用的Spring Boot。

          為什么要用Istio?

          目前,對于Java技術(shù)棧來說,構(gòu)建微服務(wù)的最佳選擇是Spring Boot而Spring Boot一般搭配目前落地案例很多的微服務(wù)框架Spring Cloud來使用。

          Spring Cloud看似很完美,但是在實際上手開發(fā)后,很容易就會發(fā)現(xiàn)Spring Cloud存在以下比較嚴(yán)重的問題:

          • 服務(wù)治理相關(guān)的邏輯存在于Spring Cloud Netflix等SDK中,與業(yè)務(wù)代碼緊密耦合。
          • SDK對業(yè)務(wù)代碼侵入太大,SDK發(fā)生升級且無法向下兼容時,業(yè)務(wù)代碼必須做出改變以適配SDK的升級——即使業(yè)務(wù)邏輯并沒有發(fā)生任何變化。
          • 各種組件令人眼花繚亂,質(zhì)量也參差不齊,學(xué)習(xí)成本太高,且組件之間代碼很難完全復(fù)用,僅僅為了實現(xiàn)治理邏輯而學(xué)習(xí)SDK也并不是很好的選擇。
          • 綁定于Java技術(shù)棧,雖然可以接入其他語言但要手動實現(xiàn)服務(wù)治理相關(guān)的邏輯,不符合微服務(wù)“可以用多種語言進(jìn)行開發(fā)”的原則。
          • Spring Cloud僅僅是一個開發(fā)框架,沒有實現(xiàn)微服務(wù)所必須的服務(wù)調(diào)度、資源分配等功能,這些需求要借助Kubernetes等平臺來完成。但Spring Cloud與Kubernetes功能上有重合,且部分功能也存在沖突,二者很難完美配合。

          替代Spring Cloud的選擇有沒有呢?有!它就是Istio。

          Istio徹底把治理邏輯從業(yè)務(wù)代碼中剝離出來,成為了獨立的進(jìn)程(Sidecar)。部署時兩者部署在一起,在一個Pod里共同運行,業(yè)務(wù)代碼完全感知不到Sidecar的存在。這就實現(xiàn)了治理邏輯對業(yè)務(wù)代碼的零侵入——實際上不僅是代碼沒有侵入,在運行時兩者也沒有任何的耦合。這使得不同的微服務(wù)完全可以使用不同語言、不同技術(shù)棧來開發(fā),也不用擔(dān)心服務(wù)治理問題,可以說這是一種很優(yōu)雅的解決方案了。

          所以,“為什么要使用Istio”這個問題也就迎刃而解了——因為Istio解決了傳統(tǒng)微服務(wù)諸如業(yè)務(wù)邏輯與服務(wù)治理邏輯耦合、不能很好地實現(xiàn)跨語言等痛點,而且非常容易使用。只要會用Kubernetes,學(xué)習(xí)Istio的使用一點都不困難。

          為什么要使用gRPC作為通信框架?

          在微服務(wù)架構(gòu)中,服務(wù)之間的通信是一個比較大的問題,一般采用RPC或者RESTful API來實現(xiàn)。

          Spring Boot可以使用RestTemplate調(diào)用遠(yuǎn)程服務(wù),但這種方式不直觀,代碼也比較復(fù)雜,進(jìn)行跨語言通信也是個比較大的問題;而gRPC相比Dubbo等常見的Java RPC框架更加輕量,使用起來也很方便,代碼可讀性高,并且與Istio和Kubernetes可以很好地進(jìn)行整合,在Protobuf和HTTP2的加持下性能也還不錯,所以這次選擇了gRPC來解決Spring Boot微服務(wù)間通信的問題。并且,雖然gRPC沒有服務(wù)發(fā)現(xiàn)、負(fù)載均衡等能力,但是Istio在這方面就非常強(qiáng)大,兩者形成了完美的互補關(guān)系。

          由于考慮到各種grpc-spring-boot-starter可能會對Spring Boot與Istio的整合產(chǎn)生不可知的副作用,所以這一次我沒有用任何的grpc-spring-boot-starter,而是直接手寫了gRPC與Spring Boot的整合。不想借助第三方框架整合gRPC和Spring Boot的可以簡單參考一下我的實現(xiàn)。

          編寫業(yè)務(wù)代碼

          首先使用Spring Initializr建立父級項目spring-boot-istio,并引入gRPC的依賴。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 https://maven.apache.org/xsd/maven-4.0.0.xsd">

              <modelVersion>4.0.0</modelVersion>
              <modules>
                  <module>spring-boot-istio-api</module>
                  <module>spring-boot-istio-server</module>
                  <module>spring-boot-istio-client</module>
              </modules>
              <parent>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-parent</artifactId>
                  <version>2.2.6.RELEASE</version>
                  <relativePath/> <!-- lookup parent from repository -->
              </parent>
              <groupId>site.wendev</groupId>
              <artifactId>spring-boot-istio</artifactId>
              <version>0.0.1-SNAPSHOT</version>
              <name>spring-boot-istio</name>
              <description>Demo project for Spring Boot With Istio.</description>
              <packaging>pom</packaging>

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

              <dependencyManagement>
                  <dependencies>
                      <dependency>
                          <groupId>io.grpc</groupId>
                          <artifactId>grpc-all</artifactId>
                          <version>1.28.1</version>
                      </dependency>
                  </dependencies>
              </dependencyManagement>
          </project>

          然后建立公共依賴模塊spring-boot-istio-api,pom文件如下,主要就是gRPC的一些依賴:

          <?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>spring-boot-istio</artifactId>
                  <groupId>site.wendev</groupId>
                  <version>0.0.1-SNAPSHOT</version>
              </parent>
              <modelVersion>4.0.0</modelVersion>

              <artifactId>spring-boot-istio-api</artifactId>

              <dependencies>
                  <dependency>
                      <groupId>io.grpc</groupId>
                      <artifactId>grpc-all</artifactId>
                  </dependency>
                  <dependency>
                      <groupId>javax.annotation</groupId>
                      <artifactId>javax.annotation-api</artifactId>
                      <version>1.3.2</version>
                  </dependency>
              </dependencies>

              <build>
                  <extensions>
                      <extension>
                          <groupId>kr.motd.maven</groupId>
                          <artifactId>os-maven-plugin</artifactId>
                          <version>1.6.2</version>
                      </extension>
                  </extensions>
                  <plugins>
                      <plugin>
                          <groupId>org.xolstice.maven.plugins</groupId>
                          <artifactId>protobuf-maven-plugin</artifactId>
                          <version>0.6.1</version>
                          <configuration>
                              <protocArtifact>com.google.protobuf:protoc:3.11.3:exe:${os.detected.classifier}</protocArtifact>
                              <pluginId>grpc-java</pluginId>
                              <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.28.1:exe:${os.detected.classifier}</pluginArtifact>
                              <protocExecutable>/Users/jiangwen/tools/protoc-3.11.3/bin/protoc</protocExecutable>
                          </configuration>
                          <executions>
                              <execution>
                                  <goals>
                                      <goal>compile</goal>
                                      <goal>compile-custom</goal>
                                  </goals>
                              </execution>
                          </executions>
                      </plugin>
                  </plugins>
              </build>
          </project>

          建立src/main/proto文件夾,在此文件夾下建立hello.proto,定義服務(wù)間的接口如下:

          syntax = "proto3";

          option java_package = "site.wendev.spring.boot.istio.api";
          option java_outer_classname = "HelloWorldService";

          package helloworld;

          service HelloWorld {
              rpc SayHello (HelloRequest) returns (HelloResponse) {}
          }

          message HelloRequest {
              string name = 1;
          }

          message HelloResponse {
              string message = 1;
          }

          很簡單,就是發(fā)送一個name返回一個帶name的message。

          然后生成服務(wù)端和客戶端的代碼,并且放到j(luò)ava文件夾下。這部分內(nèi)容可以參考gRPC的官方文檔。

          有了API模塊之后,就可以編寫服務(wù)提供者(服務(wù)端)和服務(wù)消費者(客戶端)了。這里我們重點看一下如何整合gRPC和Spring Boot。

          服務(wù)端

          業(yè)務(wù)代碼非常簡單:

          /**
           * 服務(wù)端業(yè)務(wù)邏輯實現(xiàn)
           *
           * @author 江文
           */

          @Slf4j
          @Component
          public class HelloServiceImpl extends HelloWorldGrpc.HelloWorldImplBase {
              @Override
              public void sayHello(HelloWorldService.HelloRequest request,
                                   StreamObserver<HelloWorldService.HelloResponse> responseObserver)
           
          {
                  // 根據(jù)請求對象建立響應(yīng)對象,返回響應(yīng)信息
                  HelloWorldService.HelloResponse response = HelloWorldService.HelloResponse
                          .newBuilder()
                          .setMessage(String.format("Hello, %s. This message comes from gRPC.", request.getName()))
                          .build();
                  responseObserver.onNext(response);
                  responseObserver.onCompleted();
                  log.info("Client Message Received:[{}]", request.getName());
              }
          }

          光有業(yè)務(wù)代碼還不行,我們還需要在應(yīng)用啟動時把gRPC Server也給一起啟動起來。首先寫一下Server端的啟動、關(guān)閉等邏輯:

          /**
           * gRPC Server的配置——啟動、關(guān)閉等
           * 需要使用<code>@Component</code>注解注冊為一個Spring Bean
           *
           * @author 江文
           */

          @Slf4j
          @Component
          public class GrpcServerConfiguration {
              @Autowired
              HelloServiceImpl service;

              /** 注入配置文件中的端口信息 */
              @Value("${grpc.server-port}")
              private int port;
              private Server server;

              public void start() throws IOException {
                  // 構(gòu)建服務(wù)端
                  log.info("Starting gRPC on port {}.", port);
                  server = ServerBuilder.forPort(port).addService(service).build().start();
                  log.info("gRPC server started, listening on {}.", port);

                  // 添加服務(wù)端關(guān)閉的邏輯
                  Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                      log.info("Shutting down gRPC server.");
                      GrpcServerConfiguration.this.stop();
                      log.info("gRPC server shut down successfully.");
                  }));
              }

              private void stop() {
                  if (server != null) {
                      // 關(guān)閉服務(wù)端
                      server.shutdown();
                  }
              }

              public void block() throws InterruptedException {
                  if (server != null) {
                      // 服務(wù)端啟動后直到應(yīng)用關(guān)閉都處于阻塞狀態(tài),方便接收請求
                      server.awaitTermination();
                  }
              }
          }

          定義好gRPC的啟動、停止等邏輯后,就可以使用CommandLineRunner把它加入到Spring Boot的啟動中去了:

          /**
           * 加入gRPC Server的啟動、停止等邏輯到Spring Boot的生命周期中
           *
           * @author 江文
           */

          @Component
          public class GrpcCommandLineRunner implements CommandLineRunner {
              @Autowired
              GrpcServerConfiguration configuration;

              @Override
              public void run(String... args) throws Exception {
                  configuration.start();
                  configuration.block();
              }
          }

          之所以要把gRPC的邏輯注冊成Spring Bean,就是因為在這里要獲取到它的實例并進(jìn)行相應(yīng)的操作。

          這樣,在啟動Spring Boot時,由于CommandLineRunner的存在,gRPC服務(wù)端也就可以一同啟動了。

          客戶端

          業(yè)務(wù)代碼同樣非常簡單:

          /**
           * 客戶端業(yè)務(wù)邏輯實現(xiàn)
           *
           * @author 江文
           */

          @RestController
          @Slf4j
          public class HelloController {
              @Autowired
              GrpcClientConfiguration configuration;

              @GetMapping("/hello")
              public String hello(@RequestParam(name = "name", defaultValue = "JiangWen", required = false) String name) {
                  // 構(gòu)建一個請求
                  HelloWorldService.HelloRequest request = HelloWorldService.HelloRequest
                          .newBuilder()
                          .setName(name)
                          .build();

                  // 使用stub發(fā)送請求至服務(wù)端
                  HelloWorldService.HelloResponse response = configuration.getStub().sayHello(request);
                  log.info("Server response received: [{}]", response.getMessage());
                  return response.getMessage();
              }
          }

          在啟動客戶端時,我們需要打開gRPC的客戶端,并獲取到channel和stub以進(jìn)行RPC通信,來看看gRPC客戶端的實現(xiàn)邏輯:

          /**
           * gRPC Client的配置——啟動、建立channel、獲取stub、關(guān)閉等
           * 需要注冊為Spring Bean
           *
           * @author 江文
           */

          @Slf4j
          @Component
          public class GrpcClientConfiguration {
              /** gRPC Server的地址 */
              @Value("${server-host}")
              private String host;

              /** gRPC Server的端口 */
              @Value("${server-port}")
              private int port;

              private ManagedChannel channel;
              private HelloWorldGrpc.HelloWorldBlockingStub stub;

              public void start() {
                  // 開啟channel
                  channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();

                  // 通過channel獲取到服務(wù)端的stub
                  stub = HelloWorldGrpc.newBlockingStub(channel);
                  log.info("gRPC client started, server address: {}:{}", host, port);
              }

              public void shutdown() throws InterruptedException {
                  // 調(diào)用shutdown方法后等待1秒關(guān)閉channel
                  channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
                  log.info("gRPC client shut down successfully.");
              }

              public HelloWorldGrpc.HelloWorldBlockingStub getStub() {
                  return this.stub;
              }
          }

          比服務(wù)端要簡單一些。

          最后,仍然需要一個CommandLineRunner把這些啟動邏輯加入到Spring Boot的啟動過程中:

          /**
           * 加入gRPC Client的啟動、停止等邏輯到Spring Boot生命周期中
           *
           * @author 江文
           */

          @Component
          @Slf4j
          public class GrpcClientCommandLineRunner implements CommandLineRunner {
              @Autowired
              GrpcClientConfiguration configuration;

              @Override
              public void run(String... args) {
                  // 開啟gRPC客戶端
                  configuration.start();
                  
                  // 添加客戶端關(guān)閉的邏輯
                  Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                      try {
                          configuration.shutdown();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }));
              }
          }

          編寫Dockerfile

          業(yè)務(wù)代碼跑通之后,就可以制作Docker鏡像,準(zhǔn)備部署到Istio中去了。

          在開始編寫Dockerfile之前,先改動一下客戶端的配置文件:

          server:
            port: 19090
          spring:
            application:
              name: spring-boot-istio-client
          server-host: ${server-host}
          server-port: ${server-port}

          接下來編寫Dockerfile:

          服務(wù)端:

          FROM openjdk:8u121-jdk
          RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
            && echo 'Asia/Shanghai' >/etc/timezone

          ADD /target/spring-boot-istio-server-0.0.1-SNAPSHOT.jar /
          ENV SERVER_PORT="18080"
          ENTRYPOINT java -jar /spring-boot-istio-server-0.0.1-SNAPSHOT.jar

          可以看到這里添加了啟動參數(shù),配合前面的配置,當(dāng)這個鏡像部署到Kubernetes集群時,就可以在Kubernetes的配合之下通過服務(wù)名找到服務(wù)端了。

          同時,服務(wù)端和客戶端的pom文件中添加:

          <build>
              <plugins>
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                      <configuration>
                          <executable>true</executable>
                      </configuration>
                  </plugin>
                  <plugin>
                      <groupId>com.spotify</groupId>
                      <artifactId>dockerfile-maven-plugin</artifactId>
                      <version>1.4.13</version>
                      <dependencies>
                          <dependency>
                              <groupId>javax.activation</groupId>
                              <artifactId>activation</artifactId>
                              <version>1.1</version>
                          </dependency>
                      </dependencies>
                      <executions>
                          <execution>
                              <id>default</id>
                              <goals>
                                  <goal>build</goal>
                                  <goal>push</goal>
                              </goals>
                          </execution>
                      </executions>
                      <configuration>
                          <repository>wendev-docker.pkg.coding.net/develop/docker/${project.artifactId}
                          </repository>
                          <tag>${project.version}</tag>
                          <buildArgs>
                              <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
                          </buildArgs>
                      </configuration>
                  </plugin>
              </plugins>
          </build>

          這樣執(zhí)行mvn clean package時就可以同時把docker鏡像構(gòu)建出來了。

          編寫部署文件

          有了鏡像之后,就可以寫部署文件了:

          服務(wù)端:

          apiVersion: v1
          kind: Service
          metadata:
            name: spring-boot-istio-server
          spec:
            type: ClusterIP
            ports:
              - name: http
                port: 18080
                targetPort: 18080
              - name: grpc
                port: 18888
                targetPort: 18888
            selector:
              app: spring-boot-istio-server
          ---
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: spring-boot-istio-server
          spec:
            replicas: 1
            selector:
              matchLabels:
                app: spring-boot-istio-server
            template:
              metadata:
                labels:
                  app: spring-boot-istio-server
              spec:
                containers:
                  - name: spring-boot-istio-server
                    image: wendev-docker.pkg.coding.net/develop/docker/spring-boot-istio-server:0.0.1-SNAPSHOT
                    imagePullPolicy: Always
                    tty: true
                    ports:
                      - name: http
                        protocol: TCP
                        containerPort: 18080
                      - name: grpc
                        protocol: TCP
                        containerPort: 18888

          主要是暴露服務(wù)端的端口:18080和gRPC Server的端口18888,以便可以從Pod外部訪問服務(wù)端。

          客戶端:

          apiVersion: v1
          kind: Service
          metadata:
            name: spring-boot-istio-client
          spec:
            type: ClusterIP
            ports:
              - name: http
                port: 19090
                targetPort: 19090
            selector:
              app: spring-boot-istio-client
          ---
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: spring-boot-istio-client
          spec:
            replicas: 1
            selector:
              matchLabels:
                app: spring-boot-istio-client
            template:
              metadata:
                labels:
                  app: spring-boot-istio-client
              spec:
                containers:
                  - name: spring-boot-istio-client
                    image: wendev-docker.pkg.coding.net/develop/docker/spring-boot-istio-client:0.0.1-SNAPSHOT
                    imagePullPolicy: Always
                    tty: true
                    ports:
                      - name: http
                        protocol: TCP
                        containerPort: 19090

          主要是暴露客戶端的端口19090,以便訪問客戶端并調(diào)用服務(wù)端。

          如果想先試試把它們部署到k8s可不可以正常訪問,可以這樣配置Ingress:

          apiVersion: networking.k8s.io/v1beta1
          kind: Ingress
          metadata:
            name: nginx-web
            annotations:
              kubernetes.io/ingress.class: "nginx"
              nginx.ingress.kubernetes.io/use-reges: "true"
              nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
              nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
              nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
              nginx.ingress.kubernetes.io/proxy-body-size: "10m"
              nginx.ingress.kubernetes.io/rewrite-target: /
          spec:
            rules:
              - host: dev.wendev.site
                http:
                  paths:
                    - path: /
                      backend:
                        serviceName: spring-boot-istio-client
                        servicePort: 19090

          Istio的網(wǎng)關(guān)配置文件與k8s不大一樣:

          apiVersion: networking.istio.io/v1alpha3
          kind: Gateway
          metadata:
            name: spring-boot-istio-gateway
          spec:
            selector:
              istio: ingressgateway
            servers:
              - port:
                  number: 80
                  name: http
                  protocol: HTTP
                hosts:
                  - "*"
          ---
          apiVersion: networking.istio.io/v1alpha3
          kind: VirtualService
          metadata:
            name: spring-boot-istio
          spec:
            hosts:
              - "*"
            gateways:
              - spring-boot-istio-gateway
            http:
              - match:
                  - uri:
                      exact: /hello
                route:
                  - destination:
                      host: spring-boot-istio-client
                      port:
                        number: 19090

          主要就是暴露/hello這個路徑,并且指定對應(yīng)的服務(wù)和端口。

          部署應(yīng)用到Istio

          首先搭建k8s集群并且安裝istio。我使用的k8s版本是1.16.0,Istio版本是最新的1.6.0-alpha.1,使用istioctl命令安裝Istio。建議跑通官方的bookinfo示例之后再來部署本項目。

          注:以下命令都是在開啟了自動注入Sidecar的前提下運行的

          我是在虛擬機(jī)中運行的k8s,所以istio-ingressgateway沒有外部ip:

          $ kubectl get svc istio-ingressgateway -n istio-system
          NAME                   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)                                                                                                                                      AGE
          istio-ingressgateway   NodePort   10.97.158.232   <none>        15020:30388/TCP,80:31690/TCP,443:31493/TCP,15029:32182/TCP,15030:31724/TCP,15031:30887/TCP,15032:30369/TCP,31400:31122/TCP,15443:31545/TCP   26h

          所以,需要設(shè)置IP和端口,以NodePort的方式訪問gateway:

          export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}')
          export SECURE_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}')
          export INGRESS_HOST=127.0.0.1
          export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT

          必須要等到兩個pod全部變?yōu)镽unning而且Ready變?yōu)?/2才算部署完成。

          接下來就可以通過

          curl -s http://${GATEWAY_URL}/hello

          訪問到服務(wù)了。如果成功返回了Hello, JiangWen. This message comes from gRPC.的結(jié)果,沒有出錯則說明部署完成。

          覺得本文對你有幫助?請分享給更多人

          關(guān)注「全棧開發(fā)者社區(qū)」加星標(biāo),提升全棧技能

          本公眾號會不定期給大家發(fā)福利,包括送書、學(xué)習(xí)資源等,敬請期待吧!

          如果感覺推送內(nèi)容不錯,不妨右下角點個在看轉(zhuǎn)發(fā)朋友圈或收藏,感謝支持。

          好文章,留言、點贊、在看和分享一條龍

          瀏覽 51
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青娱乐亚洲精品视频在线观看 | 久久精品毛片 | 小泽玛利亚黑人喷水 | 国产无码操 | 俺去俺来也在线WWW |