搭建SpringCloud项目,并实现自动化部署[通俗易懂]

搭建SpringCloud项目,并实现自动化部署[通俗易懂]前一阵子一直在忙着做项目,最近有空,把项目中用到的SpringBoot微服务相关架构再梳理一遍。项目资源:https://github.com/sunroyi/SpringCloud主要分为以下几步:(1)搭建SpringBootService,这里是各个微服务的业务逻辑。(这里搭建了2个Service,用来测试熔断)(2)搭建SpringBootEureka,用来发现服务。(…

大家好,又见面了,我是你们的朋友全栈君。

前一阵子一直在忙着做项目,最近有空,把项目中用到的SpringBoot微服务相关架构再梳理一遍。

项目资源:https://github.com/sunroyi/SpringCloud

主要分为以下几步:

(1)搭建SpringBootService,这里是各个微服务的业务逻辑。(这里搭建了2个Service,用来测试熔断)

(2)搭建SpringBootEureka,用来发现服务。

(3)搭建SpringBootConfig,用于动态维护配置文件。

(4)搭建SpringBootConsumer,这个不是必须的,只有当一个更大的业务需要调用其他多个微服务Service时才需要搭建。

         可以通过Ribbon+RestTemplate,Feign两种方式进行微服务之间的调用。可以加入Hystrix进行熔断处理。

(5)搭建SpringBootZuul,用于路由控制。可以加入Hystrix用于熔断处理。

(6)使用Jenkins进行代码的自动化部署。

——————————————————————————————————————

在项目搭建之前,需要先了解Spring Cloud中Hystrix、Ribbon以及Feign它们三者之间在处理微服务调用超时从而触发熔断降级的关系。这里我借用一下别人的描述:https://www.jianshu.com/p/31dfb595170c

Ribbon:

它的作用是负载均衡,会帮你在每次请求时选择一台机器,均匀的把请求分发到各个机器上Ribbon的负载均衡默认使用的最经典的Round Robin轮询算法。这是啥?简单来说,就是如果订单服务对库存服务发起10次请求,那就先让你请求第1台机器、然后是第2台机器、第3台机器、第4台机器、第5台机器,接着再来—个循环,第1台机器、第2台机器。。。以此类推。

Feign:

Feign客户端是一个web声明式http远程调用工具,提供了接口和注解方式进行调用(用来调用其他服务)

Feign的一个关键机制就是使用了动态代理。

1. 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理

2. 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心

3. Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址

4. 最后针对这个地址,发起请求、解析响应

Hystrix:

在微服务架构里,一个系统会有很多的服务。以本文的业务场景为例:订单服务在一个业务流程里需要调用三个服务。现在假设订单服务自己最多只有100个线程可以处理请求,然后呢,积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常。

咱们一起来分析一下,这样会导致什么问题?

1. 如果系统处于高并发的场景下,大量请求涌过来的时候,订单服务的100个线程都会卡在请求积分服务这块。导致订单服务没有一个线程可以处理请求

2. 然后就会导致别人请求订单服务的时候,发现订单服务也挂了,不响应任何请求了

上面这个,就是微服务架构中恐怖的服务雪崩问题

这么多服务互相调用,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。比如积分服务挂了,会导致订单服务的线程全部卡在请求积分服务这里,没有一个线程可以工作,瞬间导致订单服务也挂了,别人请求订单服务全部会卡住,无法响应。

Hystrix是隔离、熔断以及降级的一个框架。啥意思呢?说白了,Hystrix会搞很多个小小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。

打个比方:现在很不幸,积分服务挂了,会咋样?

当然会导致订单服务里的那个用来调用积分服务的线程都卡死不能工作了啊!但是由于订单服务调用库存服务、仓储服务的这两个线程池都是正常工作的,所以这两个服务不会受到任何影响。

这个时候如果别人请求订单服务,订单服务还是可以正常调用库存服务扣减库存,调用仓储服务通知发货。只不过调用积分服务的时候,每次都会报错。但是如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!所以我们直接对积分服务熔断不就得了,比如在5分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。

——————————————————————————————————————

我们知道在Spring Cloud微服务体系下,微服务之间的互相调用可以通过Feign进行声明式调用,在这个服务调用过程中Feign会通过Ribbon从服务注册中心获取目标微服务的服务器地址列表,之后在网络请求的过程中Ribbon就会将请求以负载均衡的方式打到微服务的不同实例上,从而实现Spring Cloud微服务架构中最为关键的功能即服务发现及客户端负载均衡调用。

另一方面微服务在互相调用的过程中,为了防止某个微服务的故障消耗掉整个系统所有微服务的连接资源,所以在实施微服务调用的过程中我们会要求在调用方实施针对被调用微服务的熔断逻辑。而要实现这个逻辑场景在Spring Cloud微服务框架下我们是通过Hystrix这个框架来实现的。

调用方会针对被调用微服务设置调用超时时间,一旦超时就会进入熔断逻辑,而这个故障指标信息也会返回给Hystrix组件,Hystrix组件会根据熔断情况判断被调微服务的故障情况从而打开熔断器,之后所有针对该微服务的请求就会直接进入熔断逻辑,直到被调微服务故障恢复,Hystrix断路器关闭为止。

——————————————————————————————————————

一. SpringBootService的搭建

1. 创建SpringBoot项目

1.1. New Project -> Maven -> Next

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

1.2. 输入GroupId,ArtifactId -> Next -> Finish

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

2. SpringBoot项目的相关配置

2.1. 在pom.xml中加入SpringBoot相关的Jar包

<?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.sun</groupId>
    <artifactId>springbootService</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </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>Edgware.SR2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <!-- 添加这个依赖之后就可以创建一个web应用程序。starter poms部分可以引入所有需要在实际项目中使用的依赖。
            spring-boot-starter-web依赖包含所有的spring-core, spring-web, spring-webmvc,嵌入的Tomcat server和其他web应用相关的库。 -->
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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


        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.7</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-jaxb-annotations</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>

        <!--Spring Cloud Config 客户端依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>

        <!--Spring Boot Actuator,感应服务端变化-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

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

</project>

 

2.2. 在resources中添加application.properties,这个文件用来进行 项目的相关配置

spring.application.name=springbootService
server.port=6001

3. 项目结构和代码

3.1. 项目目录结构

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

其中,common目录下是一些共通代码,我这里直接拿过来用了。

sun下面的controller提供对外接口,entity定义实体类。

ComsumerClientApplication在sun目录下,是SpringBoot项目的启动文件。

 

3.2. 相关Java代码

(1)ComsumerClientApplication.java

package sun;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan
@EnableDiscoveryClient
@RefreshScope   //开启配置更新功能
public class ConsumerClientApplication {

    public static void main(String[] args) {

        SpringApplication.run(ConsumerClientApplication.class, args);
    }
}

(2)ServiceController.java

/**
 * Copyright &copy; 2012-2014 <a href="https://github.com/thinkgem/jeesite">JeeSite</a> All rights reserved.
 */
package sun.controller;

import common.entity.RestfulResult;
import common.utils.CommUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.entity.ServiceInfo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController // 重要,如果用Controller会404
@RequestMapping(value = "service")
public class ServiceController {

    @RequestMapping(value = "hello")
    public void login(HttpServletRequest request, HttpServletResponse response,
                      @RequestBody ServiceInfo serviceInfo) {

        RestfulResult restfulResult = new RestfulResult();

        try {
            restfulResult.setData("Service1:Welcome " + serviceInfo.getName() + "!");

        } catch (Exception e) {
            e.printStackTrace();
        }

        CommUtils.printDataJason(response, restfulResult);
    }

    @RequestMapping(value = "rest")
    public String rest(@RequestBody ServiceInfo serviceInfo){

        return "Service1:Welcome " + serviceInfo.getName() + " !";
    }
}

(3)ServiceInfo.java

package sun.entity;

public class ServiceInfo {
    private static final long serialVersionUID = 1L;

    private String name;

    public String getName() {
        return name;
    }

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

(4)RestfulResult.java

package common.entity;


public class RestfulResult {

	private String result = "Success";
	private String message;
	private Object data;		// 返回数据
	private int cntPage;		// page数
	private long cntData;		// 返回数据总数
	
	public String getResult() {
		return result;
	}
	public void setResult(String result) {
		this.result = result;
	}
	public String getMessage() {
		return message;
	}
	public void setMessage(String message) {
		this.message = message;
	}
	public Object getData() {
		return data;
	}
	public void setData(Object data) {
		this.data = data;
	}
	public int getCntPage() {
		return cntPage;
	}
	public void setCntPage(int cntPage) {
		this.cntPage = cntPage;
	}
	public long getCntData() {
		return cntData;
	}
	public void setCntData(long cntData) {
		this.cntData = cntData;
	}
}

(5)CommonUtils.java

package common.utils;


import javax.servlet.http.HttpServletResponse;

public class CommUtils {

	// JSON格式化
	public static String printDataJason(HttpServletResponse response,
			Object item) {
		try {

			JsonUtils.renderString(response, item);

		} catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

	/**
	 * 随机生成6位随机验证码
	 *
	 */
	public static String createRandomVcode(int len) {
		// 验证码
		String vcode = "";
		for (int i = 0; i < len; i++) {
			vcode = vcode + (int) (Math.random() * 9);
		}
		return vcode;
	}
}

(6)JsonUtils.java

package common.utils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JsonUtils {
    public JsonUtils() {
    }

    public static String renderString(HttpServletResponse response, Object object) {
        return renderString(response, JsonMapper.toJsonString(object), "application/json");
    }

    public static String renderString(HttpServletResponse response, String string, String type) {
        try {
            response.setContentType(type);
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
            return null;
        } catch (IOException var4) {
            return null;
        }
    }
}

(7)JsonMapper.java

package common.utils;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.util.JSONPObject;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.TimeZone;

public class JsonMapper extends ObjectMapper {
    private static final long serialVersionUID = 1L;
    private static Logger logger = LoggerFactory.getLogger(JsonMapper.class);
    private static JsonMapper mapper;

    public JsonMapper() {
        this(Include.NON_EMPTY);
    }

    public JsonMapper(Include include) {
        if (include != null) {
            this.setSerializationInclusion(include);
        }

        this.enableSimple();
        this.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        this.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
            public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
                jgen.writeString("");
            }
        });
        this.registerModule((new SimpleModule()).addSerializer(String.class, new JsonSerializer<String>() {
            public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
                jgen.writeString(StringEscapeUtils.unescapeHtml4(value));
            }
        }));
        this.setTimeZone(TimeZone.getDefault());
    }

    public static JsonMapper getInstance() {
        if (mapper == null) {
            mapper = (new JsonMapper()).enableSimple();
        }

        return mapper;
    }

    public static JsonMapper nonDefaultMapper() {
        if (mapper == null) {
            mapper = new JsonMapper(Include.NON_DEFAULT);
        }

        return mapper;
    }

    public String toJson(Object object) {
        try {
            return this.writeValueAsString(object);
        } catch (IOException var3) {
            logger.warn("write to json string error:" + object, var3);
            return null;
        }
    }

    public <T> T fromJson(String jsonString, Class<T> clazz) {
        if (StringUtils.isEmpty(jsonString)) {
            return null;
        } else {
            try {
                return this.readValue(jsonString, clazz);
            } catch (IOException var4) {
                logger.warn("parse json string error:" + jsonString, var4);
                return null;
            }
        }
    }

    public <T> T fromJson(String jsonString, JavaType javaType) {
        if (StringUtils.isEmpty(jsonString)) {
            return null;
        } else {
            try {
                return this.readValue(jsonString, javaType);
            } catch (IOException var4) {
                logger.warn("parse json string error:" + jsonString, var4);
                return null;
            }
        }
    }

    public JavaType createCollectionType(Class<?> collectionClass, Class... elementClasses) {
        return this.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }

    public <T> T update(String jsonString, T object) {
        try {
            return this.readerForUpdating(object).readValue(jsonString);
        } catch (JsonProcessingException var4) {
            logger.warn("update json string:" + jsonString + " to object:" + object + " error.", var4);
        } catch (IOException var5) {
            logger.warn("update json string:" + jsonString + " to object:" + object + " error.", var5);
        }

        return null;
    }

    public String toJsonP(String functionName, Object object) {
        return this.toJson(new JSONPObject(functionName, object));
    }

    public JsonMapper enableEnumUseToString() {
        this.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
        this.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
        return this;
    }

    public JsonMapper enableJaxbAnnotation() {
        JaxbAnnotationModule module = new JaxbAnnotationModule();
        this.registerModule(module);
        return this;
    }

    public JsonMapper enableSimple() {
        this.configure(Feature.ALLOW_SINGLE_QUOTES, true);
        this.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        return this;
    }

    public ObjectMapper getMapper() {
        return this;
    }

    public static String toJsonString(Object object) {
        return getInstance().toJson(object);
    }

    public static Object fromJsonString(String jsonString, Class<?> clazz) {
        return getInstance().fromJson(jsonString, clazz);
    }
}

4. 项目启动和测试

4.1. 运行Debug,启动ConsumerClientApplication.java

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

"C:\Program Files\Java\jdk1.8.0_66\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:54364,suspend=y,server=n -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=54362 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -javaagent:D:\ideaIU-2018.1.win\lib\rt\debugger-agent.jar=file:/C:/Users/Administrator/AppData/Local/Temp/capture607016.props -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_66\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_66\jre\lib\rt.jar;D:\workspace20160509\springbootService\target\classes;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-web\1.5.8.RELEASE\spring-boot-starter-web-1.5.8.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter\1.5.8.RELEASE\spring-boot-starter-1.5.8.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot\1.5.8.RELEASE\spring-boot-1.5.8.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\1.5.8.RELEASE\spring-boot-autoconfigure-1.5.8.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-logging\1.5.8.RELEASE\spring-boot-starter-logging-1.5.8.RELEASE.jar;C:\Users\Administrator\.m2\repository\ch\qos\logback\logback-classic\1.1.11\logback-classic-1.1.11.jar;C:\Users\Administrator\.m2\repository\ch\qos\logback\logback-core\1.1.11\logback-core-1.1.11.jar;C:\Users\Administrator\.m2\repository\org\slf4j\jcl-over-slf4j\1.7.25\jcl-over-slf4j-1.7.25.jar;C:\Users\Administrator\.m2\repository\org\slf4j\jul-to-slf4j\1.7.25\jul-to-slf4j-1.7.25.jar;C:\Users\Administrator\.m2\repository\org\slf4j\log4j-over-slf4j\1.7.25\log4j-over-slf4j-1.7.25.jar;C:\Users\Administrator\.m2\repository\org\yaml\snakeyaml\1.17\snakeyaml-1.17.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\1.5.8.RELEASE\spring-boot-starter-tomcat-1.5.8.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\8.5.23\tomcat-embed-core-8.5.23.jar;C:\Users\Administrator\.m2\repository\org\apache\tomcat\tomcat-annotations-api\8.5.23\tomcat-annotations-api-8.5.23.jar;C:\Users\Administrator\.m2\repository\org\apache\tomcat\embed\tomcat-embed-el\8.5.23\tomcat-embed-el-8.5.23.jar;C:\Users\Administrator\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\8.5.23\tomcat-embed-websocket-8.5.23.jar;C:\Users\Administrator\.m2\repository\org\hibernate\hibernate-validator\5.3.5.Final\hibernate-validator-5.3.5.Final.jar;C:\Users\Administrator\.m2\repository\javax\validation\validation-api\1.1.0.Final\validation-api-1.1.0.Final.jar;C:\Users\Administrator\.m2\repository\org\jboss\logging\jboss-logging\3.3.1.Final\jboss-logging-3.3.1.Final.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\classmate\1.3.4\classmate-1.3.4.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.8.10\jackson-databind-2.8.10.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-web\4.3.12.RELEASE\spring-web-4.3.12.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-aop\4.3.12.RELEASE\spring-aop-4.3.12.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-beans\4.3.12.RELEASE\spring-beans-4.3.12.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-context\4.3.12.RELEASE\spring-context-4.3.12.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-webmvc\4.3.12.RELEASE\spring-webmvc-4.3.12.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-expression\4.3.12.RELEASE\spring-expression-4.3.12.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-core\4.3.12.RELEASE\spring-core-4.3.12.RELEASE.jar;C:\Users\Administrator\.m2\repository\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar;C:\Users\Administrator\.m2\repository\com\alibaba\fastjson\1.2.7\fastjson-1.2.7.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\module\jackson-module-jaxb-annotations\2.8.10\jackson-module-jaxb-annotations-2.8.10.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.8.10\jackson-core-2.8.10.jar;C:\Users\Administrator\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.8.0\jackson-annotations-2.8.0.jar;D:\ideaIU-2018.1.win\lib\idea_rt.jar" sun.ConsumerClientApplication
Connected to the target VM, address: '127.0.0.1:54364', transport: 'socket'

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.8.RELEASE)

2019-07-09 14:17:10.710  INFO 923048 --- [           main] sun.ConsumerClientApplication            : Starting ConsumerClientApplication on PC-Sun with PID 923048 (D:\workspace20160509\springbootService\target\classes started by Administrator in D:\workspace20160509\springbootPService)
2019-07-09 14:17:10.763  INFO 923048 --- [           main] sun.ConsumerClientApplication            : No active profile set, falling back to default profiles: default
2019-07-09 14:17:11.754  INFO 923048 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5db6b9cd: startup date [Tue Jul 09 14:17:11 CST 2019]; root of context hierarchy
2019-07-09 14:17:22.203  INFO 923048 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 6000 (http)
2019-07-09 14:17:22.285  INFO 923048 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-07-09 14:17:22.312  INFO 923048 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23
2019-07-09 14:17:23.143  INFO 923048 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-07-09 14:17:23.145  INFO 923048 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 11433 ms
2019-07-09 14:17:24.072  INFO 923048 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2019-07-09 14:17:24.089  INFO 923048 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2019-07-09 14:17:24.091  INFO 923048 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2019-07-09 14:17:24.092  INFO 923048 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2019-07-09 14:17:24.092  INFO 923048 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2019-07-09 14:17:25.239  INFO 923048 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5db6b9cd: startup date [Tue Jul 09 14:17:11 CST 2019]; root of context hierarchy
2019-07-09 14:17:25.376  INFO 923048 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/service/hello]}" onto public void sun.controller.ServiceController.login(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,sun.entity.ServiceInfo)
2019-07-09 14:17:25.380  INFO 923048 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2019-07-09 14:17:25.381  INFO 923048 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2019-07-09 14:17:25.440  INFO 923048 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2019-07-09 14:17:25.440  INFO 923048 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2019-07-09 14:17:25.520  INFO 923048 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2019-07-09 14:17:25.896  INFO 923048 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2019-07-09 14:17:26.094  INFO 923048 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 6000 (http)
2019-07-09 14:17:26.100  INFO 923048 --- [           main] sun.ConsumerClientApplication            : Started ConsumerClientApplication in 17.811 seconds (JVM running for 28.604)

4.2. 调用localhost:6001/service/hello来测试

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

5. 项目打包和执行

5.1. 打包

Maven Projects -> package

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

执行结果:

[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ springbootService ---
[INFO] Building jar: D:\workspace20160509\springbootService\target\springbootService-1.0-SNAPSHOT.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:1.5.8.RELEASE:repackage (default) @ springbootService ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 26.847 s
[INFO] Finished at: 2019-07-09T14:36:54+08:00
[INFO] Final Memory: 29M/250M
[INFO] ------------------------------------------------------------------------

Process finished with exit code 0

找到jar包目录,创建start.bat文件

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

start.bat内容:

java -jar springbootService-1.0-SNAPSHOT.jar

5.2. 执行start.bat启动

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

6. 项目复制

为了在下面测试Hystrix熔断,这里复制一份springbootService2项目,区别有两个地方:

ServiceController:
/**
 * Copyright &copy; 2012-2014 <a href="https://github.com/thinkgem/jeesite">JeeSite</a> All rights reserved.
 */
package sun.controller;

import common.entity.RestfulResult;
import common.utils.CommUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.entity.ServiceInfo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController // 重要,如果用Controller会404
@RequestMapping(value = "service")
public class ServiceController {

    @RequestMapping(value = "hello")
    public void login(HttpServletRequest request, HttpServletResponse response,
                      @RequestBody ServiceInfo serviceInfo) {

        RestfulResult restfulResult = new RestfulResult();

        try {
            restfulResult.setData("Service2:Welcome " + serviceInfo.getName() + "!");

        } catch (Exception e) {
            e.printStackTrace();
        }

        CommUtils.printDataJason(response, restfulResult);
    }

    @RequestMapping(value = "rest")
    public String rest(@RequestBody ServiceInfo serviceInfo){

        return "Service2:Welcome " + serviceInfo.getName() + " !";
    }
}

application.properties:

spring.application.name=springbootService
server.port=6006

eureka.client.service-url.defaultZone=http://localhost:5000/eureka/

二. 搭建Eureka

1. 创建Maven项目springbootEureka

同springbootService

2. 项目结构和代码

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

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.sun</groupId>
    <artifactId>springbootEureka</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.12.RELEASE</version>
    </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>Edgware.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
            <version>1.3.5.RELEASE</version>
        </dependency>
    </dependencies>

</project>

application.properties

server.port=5000
eureka.instance.hostname=localhost
#是否向服务注册中心注册自己,默认为true
eureka.client.register-with-eureka=false
#是否检索服务
eureka.client.fetch-registry=true
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

ConsumerClientApplication.java

package sun;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class ConsumerClientApplication {

    public static void main(String[] args) {

        SpringApplication.run(ConsumerClientApplication.class, args);
    }
}

 

3. 启动Eureka

3.1. 运行Debug,启动ConsumerClientApplication.java

3.2. 打开http://localhost:5000,表示正常启动

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

4. 修改springbootService,使Eureka可以发现此服务

pom.xml中增加


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>

application.properties中增加

eureka.client.service-url.defaultZone=http://localhost:5000/eureka/

ConsumerClientApplication.java中增加

@EnableDiscoveryClient
package sun;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@ServletComponentScan
@EnableDiscoveryClient
public class ConsumerClientApplication {

    public static void main(String[] args) {

        SpringApplication.run(ConsumerClientApplication.class, args);
    }
}

启动后,在Eureka中可以看到springbootService已注册

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

三. 搭建SpringBootConfig配置中心

我这里用的是SVN,在config目录下创建文件springbootService-release.properties。

将springbootService的application.properties的内容复制进去。

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

为了与原文件区分看效果,我这里将server.port设为6003(原来是6001)

spring.application.name=springbootService
server.port=6003

eureka.client.service-url.defaultZone=http://localhost:5000/eureka/

配置中心其实就是读取SVN上的文件后,发送给其他服务,让他们读取。

1. 创建Maven项目springbootConfig

同springbootService

2. 项目结构和代码

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

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.sun</groupId>
    <artifactId>springbootConfig</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.12.RELEASE</version>
    </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>Edgware.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.tmatesoft.svnkit</groupId>
            <artifactId>svnkit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <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>

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

application.yml

server:
  port: 6002
spring:
  application:
    name: springbootConfig
  profiles:
    active: subversion
  cloud:
    config:
      server:
        svn:
          uri: https://192.168.3.97/svn/SourceCode/SMPH/Beats/trunk/test/config
          #username: *****
          #password: *****
          default-label:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:5000/eureka/
  instance:
    preferIpAddress: true
    instance-id: ${spring.cloud.client.ipAddress}:${server.port}
    lease-expiration-duration-in-seconds: 30
    lease-renewal-interval-in-seconds: 30

ConsumerClientApplication

package sun;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConsumerClientApplication {

    public static void main(String[] args) {

        SpringApplication.run(ConsumerClientApplication.class, args);
    }
}

3. 启动Config配置中心

3.1. Eureka中能够看到Config配置中心已注册

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

3.2. 打开http://localhost:6002/springbootService/release,能够看到配置文件的内容

{"name":"springbootService","profiles":["release"],"label":null,"version":"523","state":null,"propertySources":[{"name":"https://192.168.3.97/svn/SourceCode/SMPH/Beats/trunk/test/config/springbootService-release.properties","source":{"server.port":"6003","eureka.client.service-url.defaultZone":"http://localhost:5000/eureka/","spring.application.name":"springbootService"}}]}

4. 修改springbootService,使它能从配置中心读取配置文件

4.1. 在resources目录下创建bootstrap.yml,用来配置读取Config配置中心下的哪个文件

这里对应localhost:6002/springbootService-release.properties文件

spring:
    application:
        name : springbootService
    cloud:
        config:
            uri : http://localhost:6002/
            profile : release

4.2. 修改pom.xml,增加

        <!--Spring Cloud Config 客户端依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>

        <!--Spring Boot Actuator,感应服务端变化-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

4.3. 在ConsumerClientApplication.java中增加

@RefreshScope   //开启配置更新功能
package sun;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;

@SpringBootApplication
@ServletComponentScan
@EnableDiscoveryClient
@RefreshScope   //开启配置更新功能
public class ConsumerClientApplication {

    public static void main(String[] args) {

        SpringApplication.run(ConsumerClientApplication.class, args);
    }
}

4.4. 启动springbootService,在Eureka中可以看到,多了个端口是6003的服务。

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

4.5. 接口可以从6003访问,原来的6001不能用了,说明配置中心的文件覆盖了本地的application.properties

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

四. 搭建SpringBootConsumerFeign,使用Feign来调用各个微服务

1.直接从springbootService复制一份,命名为springbootConsumerFeign

别忘了把配置里面的名字都改掉

2. 项目结构和代码

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

2.1. 在pom中添加Feign

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

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

        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
            <version>RELEASE</version>
        </dependency>

2.2. 修改application.properties

spring.application.name=springbootConsumerFeign
server.port=6004

feign.hystrix.enabled=true

eureka.client.service-url.defaultZone=http://localhost:5000/eureka/

2.3. 在ConsumerClientApplication.java中增加

@EnableFeignClients
package sun;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

@SpringBootApplication
@ServletComponentScan
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerClientApplication {

    public static void main(String[] args) {

        SpringApplication.run(ConsumerClientApplication.class, args);
    }
}

2.4. 在sun目录下添加client目录,并新建文件ServiceFeignClient

这里是声明接口,指向springbootService的接口

package sun.client;

import common.entity.RestfulResult;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import sun.entity.ServiceInfo;

@Component
@FeignClient(value = "springbootService", fallback=ServiceFallback.class) //这里的value对应调用服务的spring.applicatoin.name
public interface ServiceFeignClient {

    @RequestMapping(value = "/service/hello")
    RestfulResult hello(@RequestBody ServiceInfo serviceInfo);

}

添加ServiceFallback.java,用于熔断发生时的处理。

package sun.client;

import common.entity.RestfulResult;
import org.springframework.stereotype.Component;
import sun.entity.ServiceInfo;

@Component
public class ServiceFallback implements ServiceFeignClient{

    @Override
    public RestfulResult hello(ServiceInfo serviceInfo) {
        RestfulResult result = new RestfulResult();
        result.setData("服务调用失败");
        return result;
    }
}

2.5. 修改ServiceController.java

/**
 * Copyright &copy; 2012-2014 <a href="https://github.com/thinkgem/jeesite">JeeSite</a> All rights reserved.
 */
package sun.controller;

import common.entity.RestfulResult;
import common.utils.CommUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.client.ServiceFeignClient;
import sun.entity.ServiceInfo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class ServiceController {

    @Autowired
    ServiceFeignClient serviceFeignClient;

    // 调用:localhost:6004/consumerService?token=1
    @RequestMapping("/consumerService")
    public void consumerService(HttpServletRequest request, HttpServletResponse response,
                       @RequestBody ServiceInfo serviceInfo){

        RestfulResult restfulResult =  serviceFeignClient.hello(serviceInfo);

        CommUtils.printDataJason(response, restfulResult);
    }
}

3. 启动springbootConsumer并测试接口

3.1. 启动后,在Eureka中发现服务

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

3.2. 调用SpringBootConsumer的接口http://localhost:6004/consumerService

连续调用,会轮询Service1和Service2

 

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

关闭Service1,看看Hystrix是否起作用:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

五. 搭建SpringBootConsumerRibbon,使用Ribbon+RestTemplate来调用各个微服务

1.直接从springbootService复制一份,命名为springbootConsumerRibbon

别忘了把配置里面的名字都改掉

2. 项目结构和代码

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

2.1. 在pom中添加Feign

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>

2.2. 修改application.properties

spring.application.name=springbootConsumerRibbon
server.port=6007

eureka.client.service-url.defaultZone=http://localhost:5000/eureka/

2.3. 修改ConsumerClientApplication.java

package sun;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@ServletComponentScan
@EnableDiscoveryClient
@EnableHystrix
public class ConsumerClientApplication {

    public static void main(String[] args) {

        SpringApplication.run(ConsumerClientApplication.class, args);
    }

    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

2.4. 修改ServiceController.java

/**
 * Copyright &copy; 2012-2014 <a href="https://github.com/thinkgem/jeesite">JeeSite</a> All rights reserved.
 */
package sun.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import sun.entity.ServiceInfo;

@RestController
public class ServiceController {

    @Autowired
    RestTemplate restTemplate;

    @Value("${server.port}")
    String port;

    // 调用:localhost:6007/consumerServiceRibbon?token=1
    @RequestMapping("/consumerServiceRibbon")
    @HystrixCommand(fallbackMethod="consumerServiceRibbonFallback")
    public String consumerServiceRibbon(@RequestBody ServiceInfo serviceInfo){
        String result = this.restTemplate.postForObject("http://springbootService/service/rest?token=1", serviceInfo, String.class);
        return result;
    }

    public String consumerServiceRibbonFallback(@RequestBody ServiceInfo serviceInfo){
        return "consumerServiceRibbon异常,端口:" + port + ",Name=" + serviceInfo.getName();
    }
}

3. 启动springbootConsumer并测试接口

3.1. 启动后,在Eureka中发现服务

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

3.2. 调用SpringBootConsumer的接口 localhost:6007/consumerServiceRibbon?token=1

连续调用,会轮询Service1和Service2

搭建SpringCloud项目,并实现自动化部署[通俗易懂]搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

 

关闭Service1,看看Hystrix是否起作用:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

六. 搭建Zuul+Hystrix

Zuul对外提供统一的服务入口,主要是用作网址重定向。

还可以通过Filter实现过滤器。

配合Hystrix实现熔断器,当服务宕机时可以做异常处理。

1. 创建Maven项目springbootZuul

同springbootService

2. 项目结构和代码

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

pom.xml

这里我把spring boot改成了2.0版本,spring cloud改成了Finchley.SR2。

<?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.sun</groupId>
    <artifactId>springbootZuul</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <!--<version>1.5.8.RELEASE</version>-->
        <version>2.0.0.RELEASE</version>
    </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.SR2</spring-cloud.version>
        <!--<spring-cloud.version>Edgware.SR2</spring-cloud.version>-->
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

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

        <!-- 配置hystrix所需依赖的包 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <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>

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

 application.yml

spring:
    application:
        name : springbootZuul

server:
    port : 6005

eureka:
    client:
        service-url:
            defaultZone : http://localhost:5000/eureka/

zuul:
    routes:
        sbService :
            path : /sbService/**
            serviceId : springbootService

ConsumerClientApplication.java

package sun;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
@RefreshScope
public class ConsumerClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerClientApplication.class, args);
    }

}

serviceFilter(过滤器,非必须)

这里判断了请求中是否带token,如果没有,则显示”there is no request token”

package sun.filter;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

@Component
public class serviceFilter extends ZuulFilter {

    private static Logger log=LoggerFactory.getLogger(serviceFilter.class);

    @Override
    public String filterType() {
        return "pre"; // 定义filter的类型,有pre、route、post、error四种
    }

    @Override
    public int filterOrder() {
        return 0; // 定义filter的顺序,数字越小表示顺序越高,越先执行
    }

    @Override
    public boolean shouldFilter() {
        return true; // 表示是否需要执行该filter,true表示执行,false表示不执行
    }

    @Override
    public Object run() {
        // filter需要执行的具体操作
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String token = request.getParameter("token");
        System.out.println(token);
        if(token==null){
            log.warn("there is no request token");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("there is no request token");
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
        log.info("ok");
        return null;
    }
}
ServiceFallbackProvider(熔断器,非必须)

当没有服务时,显示”Sorry, the service is unavailable now.”

package sun.fallbackProvider;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

@Component
public class ServiceFallbackProvider implements FallbackProvider {
    @Override
    // 指定熔断器功能应用于哪些路由的服务
    public String getRoute() {
        // 这里只针对"springbootService"服务进行熔断
        // 如果需要针对所有服务熔断,则return "*"
        return "springbootService";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        System.out.println("route:"+route);

        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "ok";
            }

            @Override
            public void close() {

            }

            @Override
            // 发生熔断式,返回的信息
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("Sorry, the service is unavailable now.".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

3. 启动springbootZuul并测试

3.1. 在Eureka上看到springbootZuul服务已启动

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

3.2. 现在可以通过统一路由访问服务了

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

3.3. 使用Filter,请求不带token时的效果:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

3.4. 使用Filter,请求带token时的效果:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

3.5. 使用Hystrix,关闭springbootService服务时的效果:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

 

六. 自动化部署Jenkins

1. 将代码上传到Github

SVN也可以,这里我就用Github了。

先去下载Git官网下载Git-2.23.0-64-bit.exe

再去下载TortoiseGit,有了这个就不用老是打命令commit了。

1.1. 图形界面的更新代码

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

Git的项目初始化就不多说了,这里介绍一下用图形界面的代码更新步骤。

① Git Commit -> “master”

② Push

 

1.2. 我在将Git从1.X升级到2.X的过程中,遇到了

SourceTree error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version

的问题,将Git版本升级后也解决不了。

后来想起来,是不是在环境变量中配置了Git1.X的路径

果然,在系统里面的Path中找到了Git的配置。

原来Git2.X以后,默认路径放到了C:\Program Files\Git下,和原来1.X的路径不一样了。

所以需要把1.X的路径配置删除,否则还是会优先使用1.X的Git。

 

2. 搭建Jenkins环境

2.1. 去Jenkins官网下载war包

2.2. 启动Jenkins的war包

java -jar jenkins.war --httpPort=8080

启动后,访问http://localhost:8080即可。

使用的时候需要注册,这些就省略不说了。

2.3. Jenkins插件的安装

需要安装Maven,Git等插件,才能和项目关联使用

Manage Jenkins -> Manage Plugins -> Available里面找

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

2.4. 创建新项目

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

在Source Code Management里面选择Git,填入自己的仓库地址:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

在Build Triggers里面,选择Poll SCM,在Schedule中填入 * * * * *    :

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

在Build中,设置pom.xml的路径

一开始可能会报找不到pom.xml的Error,因为Jenkins需要先从Git库中同步项目,等同步完以后,Error就会自动消失。

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

在Post Steps中,Add post-build stemp -> Execute Windows bath command

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

添加的windows命令如下:

D:\JenkinsTest\process.bat 6001
D:
del D:\JenkinsTest\springbootService-1.0-SNAPSHOT.jar
copy C:\Users\Administrator\.jenkins\workspace\SpringbootService\target\springbootService-1.0-SNAPSHOT.jar D:\JenkinsTest\springbootService-1.0-SNAPSHOT.jar
SET BUILD_ID=donKillMe
start javaw -Dhudson.util.ProcessTree.disable=true -jar D:\JenkinsTest\springbootService-1.0-SNAPSHOT.jar

效果如图:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

其中,D:\JenkinsTest\process.bat的内容如下:

::demo
@echo off
::延迟环境变量扩展
setlocal enabledelayedexpansion
for /f "delims=  tokens=1" %%i in ('netstat -aon ^| findstr %1') do (
set a=%%i)
::判断服务是否已经启动,如果启动则杀掉进程
if defined a (taskkill /F /pid "!a:~71,7!") else (echo Service does not exist)
::等待你按任意键结束
pause>nul
::执行时后面带上端口即可

命令说明:

① 创建D:\JenkinsTest目录

② process.bat 6001 

是为了查找是否有占用6001端口的进程,如果有,则关闭6001端口的进程,因为我这里的Service启用的是6001端口。

③ 删除D:\JenkinsTest目录下的Service的Jar包,并将Jenkins自动打包生成的Jar包拷过来

然后通过Start javaw -jar *.jar命令在后台启动jar包。

④ 由于Jenkins默认在自动Build完成后,会关闭所有子进程,所以用下面这个命令可以避免Service被关闭

SET BUILD_ID=donKillMe

⑤ 据说start javaw -Dhudson.util.ProcessTree.disable=true -jar *.jar也有用,但是我用下来好像子进程还是被关闭了。

 

3. 测试

现在,当我更新代码到Git上以后,Jenkins就会自动将代码打包成Jar,然后执行我预先写好的命令自动部署启动。

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

在Console Output中可以看到Jenkins的执行内容:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

Started by user sun wenli
Running as SYSTEM
Building in workspace C:\Users\Administrator\.jenkins\workspace\SpringbootService
No credentials specified
 > git.exe rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository
 > git.exe config remote.origin.url https://github.com/sunroyi/springbootService.git # timeout=10
Fetching upstream changes from https://github.com/sunroyi/springbootService.git
 > git.exe --version # timeout=10
 > git.exe fetch --tags --force --progress https://github.com/sunroyi/springbootService.git +refs/heads/*:refs/remotes/origin/*
 > git.exe rev-parse "refs/remotes/origin/master^{commit}" # timeout=10
 > git.exe rev-parse "refs/remotes/origin/origin/master^{commit}" # timeout=10
Checking out Revision 7ee3c59e4573b4256e6949ebbaeb99c9d398a841 (refs/remotes/origin/master)
 > git.exe config core.sparsecheckout # timeout=10
 > git.exe checkout -f 7ee3c59e4573b4256e6949ebbaeb99c9d398a841
Commit message: "3"
 > git.exe rev-list --no-walk 7ee3c59e4573b4256e6949ebbaeb99c9d398a841 # timeout=10
Parsing POMs
Established TCP socket on 58566
[SpringbootService] $ java -cp C:\Users\Administrator\.jenkins\plugins\maven-plugin\WEB-INF\lib\maven31-agent-1.13.jar;D:\maven\apache-maven-3.1.1\boot\plexus-classworlds-2.5.1.jar;D:\maven\apache-maven-3.1.1/conf/logging jenkins.maven3.agent.Maven31Main D:\maven\apache-maven-3.1.1 C:\Users\Administrator\.jenkins\war\WEB-INF\lib\remoting-3.29.jar C:\Users\Administrator\.jenkins\plugins\maven-plugin\WEB-INF\lib\maven31-interceptor-1.13.jar C:\Users\Administrator\.jenkins\plugins\maven-plugin\WEB-INF\lib\maven3-interceptor-commons-1.13.jar 58566
<===[JENKINS REMOTING CAPACITY]===>channel started
Executing Maven:  -B -f C:\Users\Administrator\.jenkins\workspace\SpringbootService\pom.xml clean package
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building springbootService 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.6.1:clean (default-clean) @ springbootService ---
[INFO] Deleting C:\Users\Administrator\.jenkins\workspace\SpringbootService\target
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ springbootService ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] Copying 1 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ springbootService ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 7 source files to C:\Users\Administrator\.jenkins\workspace\SpringbootService\target\classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ springbootService ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\Administrator\.jenkins\workspace\SpringbootService\src\test\resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ springbootService ---
[INFO] No sources to compile
[INFO] 
[INFO] --- maven-surefire-plugin:2.18.1:test (default-test) @ springbootService ---
[INFO] No tests to run.
[JENKINS] Recording test results
[INFO] 
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ springbootService ---
[INFO] Building jar: C:\Users\Administrator\.jenkins\workspace\SpringbootService\target\springbootService-1.0-SNAPSHOT.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:1.5.8.RELEASE:repackage (default) @ springbootService ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 34.060s
[INFO] Finished at: Thu Sep 12 11:27:02 CST 2019
[INFO] Final Memory: 37M/403M
[INFO] ------------------------------------------------------------------------
Waiting for Jenkins to finish collecting data
[JENKINS] Archiving C:\Users\Administrator\.jenkins\workspace\SpringbootService\pom.xml to com.sun/springbootService/1.0-SNAPSHOT/springbootService-1.0-SNAPSHOT.pom
[JENKINS] Archiving C:\Users\Administrator\.jenkins\workspace\SpringbootService\target\springbootService-1.0-SNAPSHOT.jar to com.sun/springbootService/1.0-SNAPSHOT/springbootService-1.0-SNAPSHOT.jar
channel stopped
[SpringbootService] $ cmd /c call C:\Users\ADMINI~1\AppData\Local\Temp\jenkins2482229857112663558.bat

C:\Users\Administrator\.jenkins\workspace\SpringbootService>D:\JenkinsTest\process.bat 6001 
成功: 已终止 PID 为 1299256 的进程。
[SpringbootService] $ cmd /c call C:\Users\ADMINI~1\AppData\Local\Temp\jenkins7623094155436157631.bat

C:\Users\Administrator\.jenkins\workspace\SpringbootService>D:

D:\workspace20160509\Jenkins>del D:\JenkinsTest\springbootService-1.0-SNAPSHOT.jar 

D:\workspace20160509\Jenkins>copy C:\Users\Administrator\.jenkins\workspace\SpringbootService\target\springbootService-1.0-SNAPSHOT.jar D:\JenkinsTest\springbootService-1.0-SNAPSHOT.jar 
已复制         1 个文件。

D:\workspace20160509\Jenkins>SET BUILD_ID=donKillMe 

D:\workspace20160509\Jenkins>start javaw -Dhudson.util.ProcessTree.disable=true -jar D:\JenkinsTest\springbootService-1.0-SNAPSHOT.jar 

D:\workspace20160509\Jenkins>exit 0 
Process leaked file descriptors. See https://jenkins.io/redirect/troubleshooting/process-leaked-file-descriptors for more information
Finished: SUCCESS

结果验证:

搭建SpringCloud项目,并实现自动化部署[通俗易懂]

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

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/140674.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号