一、搭建过程
1.1 创建方式
所有模块的创建均为空的maven项目,父项目中的src文件夹可直接删除
1.2 依赖管理
dependencies:当前项目与其子项目中都会添加改依赖(子项目会从父项目中继承)
dependencyManagement:只声明依赖,并不实先引入,然后子项目中添加该依赖不写版本号,则继承父项目中所声明的版本,若子项目有指定版本,则优先使用自己的;
ps: 我们可以再父项目中声明依赖,而不引入,做到版本集中统一管理;
1.3 按项目结构图创建出各个模块
对于其他模块的,可以选择直接复制一个模块文件夹,然后修改文件夹名称以及pom.xml中相关项目名,最后在父项目pom中添加一行module元素指向本模块即可;
1.4 将maven构建成springboot项目,父项目/pom.xml中
(可以构建一个springboot项目来参考一下):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.15.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
1.5 父项目中声明SpringCloud相关依赖
<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>
1.6 Eureka
1.6.1 eureka-server中:
- 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- 添加springBoot启动类,并增加注解
@EnableEurekaServer
- yml配置:
eureka:
instance:
hostname: ${EUREKA_HOSTNAME:${spring.cloud.client.ip-address}}
# ip-address: ${eureka.instance.hostname}
server:
enable-self-preservation: false #关闭服务器自我保护(生产环境开启)
eviction-interval-timer-in-ms: 30000 #清理间隔(单位毫秒,默认是60*1000)5秒将客户端剔除的服务在服务注册列表中剔除#
response-cache-update-interval-ms: 30000 #eureka server刷新readCacheMap的时间,注意,client读取的是readCacheMap,这个时间决定了多久会把readWriteCacheMap的缓存更新到readCacheMap上 #eureka server刷新readCacheMap的时间,注意,client读取的是readCacheMap,这个时间决定了多久会把readWriteCacheMap的缓存更新到readCacheMap上默认30s
response-cache-auto-expiration-in-seconds: 180 #eureka server缓存readWriteCacheMap失效时间,这个只有在这个时间过去后缓存才会失效,失效前不会更新,过期后从registry重新读取注册服务信息,registry是一个ConcurrentHashMap。
renewal-percent-threshold: 0.85 # 指定每分钟需要收到的续约次数的阈值,默认值就是:0.85
client:
service-url:
defaultZone: ${EUREKA_URL:http://${eureka.instance.hostname}:${server.port}/eureka}
register-with-eureka: false #是否将自己注册到Eureka服务中,默认为true
fetch-registry: false #是否从Eureka中获取注册信息,默认为true
1.6.2 eureka-client中:
- 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 启动类添加注解:
@EnableDiscoveryClient
- yml配置:
eureka:
instance:
hostname: ${HOSTNAME:${spring.cloud.client.ip-address}}
# hostname: ${HOSTNAME:127.0.0.1}
ip-address: ${eureka.instance.hostname}
prefer-ip-address: true
instance-id: ${spring.application.name}:${eureka.instance.hostname}:${server.port}
# instance-id: ${spring.application.name}:${docker.ipAddress}:${spring.application.instance_id:${server.port}}
lease-renewal-interval-in-seconds: 30 #续约间隔时间 s
lease-expiration-duration-in-seconds: 90 #续约超时时间 s
client:
serviceUrl:
defaultZone: ${DEFAULTZONE:http://admin:123456@${eureka.instance.hostname}:9001/eureka/}
registry-fetch-interval-seconds: 30 # 间隔多久去拉取服务注册信息,默认为30秒
二、zuul的配置与使用
2.1 配置
- 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
- 启动类添加注解
@EnableZuulProxy
- yml配置(包含重试)
######## zuul的配置 start ########
ribbon:
eager-load:
enabled: true
ReadTimeout: ${READ_TIMEOUT:30000} # 获取到响应内容的超时时间
ConnectTimeout: ${CONNECTION_TIMEOUT:1000} # 连接超时时间
MaxAutoRetries: 0 # 在当前服务重试的次数
MaxAutoRetriesNextServer: 1 # 默认轮询去重选列表中服务的次数
OkToRetryOnAllOperations: ${ALLRETRY:false} #是否所有操作都重试 (默认false)
# 使用okhttp: (还需要引入相关依赖,且okhttp默认开启重试)
# http:
# client:
# enabled: false
# okhttp:
# enabled: true
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: ${THREAD_TIMEOUT:30000}
zuul:
host: # routes使用url时走这个超时配置
socket-timeout-millis: 30000
connect-timeout-millis: 1000
max-per-route-connections: 100 # 适用于ApacheHttpClient,如果是okhttp无效。每个服务的http客户端连接池最大连接,默认是200
max-total-connections: 500 # 适用于ApacheHttpClient,如果是okhttp无效。每个route可用的最大连接数,默认值是20
ribbon:
eager-load:
enabled: true
# 开启自动重试
retryable: true
# 自定义路由转发规则
routes:
user:
path: /uc/**
#url: http://server-b-3:9006
sensitiveHeaders:
service-id: admin-user
ebankpay:
path: /pay/**
#url: http://server-b-3:9008
sensitiveHeaders:
service-id: admin-ebankpay
product:
path: /product/**
#url: http://server-b-3:9011
sensitiveHeaders:
service-id: admin-product
add-proxy-headers: true
#因为zuul有默认的隐射机制,如果没有以下的配置,那么访问http://ip:gatewayPort/c/也可以访问到你的c服务,如果你不想向外界暴露除你配置的隐射之外的服务,可以加上zuul.ignored-services:*
ignoredServices: '*'
######## zuul的配置 end ########
2.2 Zuul重试配置需要添加依赖
依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
yml配置(重试次数配置见上方zuul的配置,这里不再全部重复写了):
zuul:
retryable: true # 开启自动重试
2.3 动态路由
@Bean
public PatternServiceRouteMapper patternServiceRouteMapper() {
// servicePattern 决定是哪个微服务eureka-client
// routePattern 怎么样的访问形式 http://localhost/eureka-client/info
return new PatternServiceRouteMapper("(?<name>.*)-service", "${name}");
}
//main方法测试路由配置是否正确
public static void main111(String[] args) {
PatternServiceRouteMapper patternServiceRouteMapper = new PatternServiceRouteMapper("(?<name>.*)-service", "${name}");
String apply = patternServiceRouteMapper.apply("user-service"); //user
System.out.println(apply);
}
2.4 ZuulFilter的使用
- 自定义filter相关类,继承ZuulFilter,重写4个方法:
@Slf4j
@Component
public class TokenZuulFilter2 extends ZuulFilter {
@Override
public String filterType() {
//指定过滤器类型
// 4种过滤器类型,分别为pre(路由之前调用),route(路由时调用),error(发生异常时调用),post(routing和error之后调用)
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
//过滤器优先级,数值越小优先级越高
return FilterConstants.SEND_ERROR_FILTER_ORDER + 1;
}
@Override
public boolean shouldFilter() {
// 这里也可以做一些判断,决定是否开启这个filter
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
//判断登录接口设置放行
StringBuffer requestURL = request.getRequestURL();
log.info("requestURL:{}", requestURL);
if (requestURL.toString().endsWith("/uc/userLogin/login")){
//请求登录的接口,放行
return false;
}
return true;
}
@Override
public Object run() throws ZuulException {
// 具体的过滤逻辑,比如对token的判断等
log.info("进入tokenZuulFilter2");
/*
// 放行方式,都可以:
return null;
return true;
return false;
ctx.setSendZuulResponse(true);
// 不放行并设置响应内容
ctx.setSendZuulResponse(false);
ctx.setResponseBody("xxxx");
*/
return null;
}
}
2.5 Zuul的降级处理
实现此接口(FallbackProvider)中的方法,详细见项目中 【GatewayFallbackProvider】 类
三、feign的配置与使用
3.1 服务的消费者(示例:order-server 调用 user-server)
- 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 启动类注解
@EnableFeignClients
- @FeignClient 的使用
@FeignClient(name = "user-server", fallback = UserTestFeignClientFallback.class)
public interface UserTestFeignClient {
@GetMapping("/adminUser/findByUsername/{username}")
ResultVo<Object> findByUsernameGet(@PathVariable("username") String username) ;
@PostMapping("/adminUser/findByUsername/{username}")
ResultVo<Object> findByUsernamePost(@PathVariable("username") String username);
}
- yml配置
######## feign的配置 start ########
feign:
hystrix:
enabled: true # 开启hystrix,使@FeignClient注解中fallback的指定降级处理生效
httpclient: #默认就使用httpclient,在pom里面添加就是httpclient,不添加就实现HttpUrlConnection
enabled: true
max-connections: 200
max-connections-per-route: 50
okhttp:
enabled: false
# client:
# config:
# default:
# readTimeout: 5000
# connectTimeout: 200
ribbon:
eager-load:
enabled: true
ReadTimeout: ${READ_TIMEOUT:30000} # 获取到响应内容的超时时间
ConnectTimeout: ${CONNECTION_TIMEOUT:1000} # 连接超时时间
MaxAutoRetries: 0 # 在当前服务重试的次数
MaxAutoRetriesNextServer: 1 # 默认轮询去重选列表中服务的次数
OkToRetryOnAllOperations: ${ALLRETRY:false} #是否所有操作都重试 (默认false)
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: ${THREAD_TIMEOUT:30000}
######## feign的配置 end ########
- 优化
Feign 的底层 构建了一个RestTemplate 对象,而RestTemplate对象并没有直接发起请求的能力,其内部使用HttpURLConnection 来实际发请求的 。Feign 里面RestTemplate 利用的使用JDK的HttpUrlConenction 发请求。 HttpUrlConnection 它的性能比较好,但是它不支持连接池特性,在并发量特别大时,需要频繁的创建连接。若没有池对象,则会创建大量的对象,导致性能下降,所以我们选有池的好一点
使用httpclient连接池,在pom里面添加依赖
<!--feign调用的httpclient连接池支持-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
3.2 服务的提供者
提供对应访问路径的Controller即可
3.3 feign-API 的抽取
- 给每个服务提供者创建对应的 feign api 模块,抽取其FeignController
- 注意:服务提供者中对api的实现,方法注解可以继承下来,但是参数注解需要加上去(不能漏)
四、重试机制、负载均衡
feign中默认使用的是RestTemplate,底层还是HttpURLConnection;
Zuul默认使用的是HttpClient,但是zuul没有开启重试,要引入其他依赖并添加配置开启重试机制;
4.1 HttpClient VS OKHttp
1、都可以使用连接池,达到优化的目的;2、ribbon都支持;
OkHttp是一个Http-client,它的主要优势:
·HTTP/2 支持允许所有访问同一主机的请求共享一个socket
·支持GZIP压缩
·响应缓存减少重复请求
OkHttp多例模式下性能更好
- HttpClient:
如果是在单例模式下,推荐使用HttpClient效率更高;原因是httpClient的创建相对来说慢一些,在单例情况下,httpClient的速度更有优势
4.2 feign使用HttpClient
- 添加依赖:
<!--feign调用的httpclient连接池支持-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
- 在添加依赖的服务中添加配置
feign:
httpclient:
enabled: true #默认就是开启的,只要引入了HttpClient的依赖,就会加载并使用httpClient
max-connections: 200 # 默认连接池最大200个
max-connections-per-route: 50 #默认单个路由请求上最大连接数50个
4.3 feign使用OKHttp
- 依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
- yml配置
feign:
httpclient:
enabled: false #关闭httpClient
max-connections: 200 #okhttp也是从这里取值 和httpclient一样
max-connections-per-route: 50 #okhttp也是从这里取值 和httpclient一样
okhttp:
enabled: true #开启okhttp
ps:部分源码信息可以查看此类 OkHttpFeignConfiguration
4.4 zuul 使用默认httpClient时,需要手动开启重试机制
- 依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
- yml配置
zuul:
retryable: true # 添加retry依赖后,开启重试
ribbon:
ReadTimeout: 30000
connectTimeout: 2000
MaxAutoRetries: 0 # 在当前服务重试的次数
MaxAutoRetriesNextServer: 1 # 默认轮询去重选列表中服务的次数
OkToRetryOnAllOperations: false # 是否所有类型请求都重试 默认false(只有GET请求重试)
4.5 zuul 使用OKHttp
- 依赖
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
- yml配置
ribbon:
http:
client:
enabled: false # 关闭httpclient
okhttp:
enabled: true # 开启okhttp
4.6 ribbon.OkToRetryOnAllOperations
- 源码查看:FeignLoadBalancer
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(FeignLoadBalancer.RibbonRequest request, IClientConfig requestConfig) {
if (this.ribbon.isOkToRetryOnAllOperations()) {
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(), requestConfig);
} else {
return !request.toRequest().httpMethod().name().equals("GET") ? new RequestSpecificRetryHandler(true, false, this.getRetryHandler(), requestConfig) : new RequestSpecificRetryHandler(true, true, this.getRetryHandler(), requestConfig);
}
}
public RequestSpecificRetryHandler(boolean okToRetryOnConnectErrors, boolean okToRetryOnAllErrors, RetryHandler baseRetryHandler, @Nullable IClientConfig requestConfig) {
this.retrySameServer = -1;
this.retryNextServer = -1;
this.connectionRelated = Lists.newArrayList(new Class[]{
SocketException.class});
Preconditions.checkNotNull(baseRetryHandler);
this.okToRetryOnConnectErrors = okToRetryOnConnectErrors;
this.okToRetryOnAllErrors = okToRetryOnAllErrors;
this.fallback = baseRetryHandler;
if (requestConfig != null) {
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetries)) {
this.retrySameServer = (Integer)requestConfig.get(CommonClientConfigKey.MaxAutoRetries);
}
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetriesNextServer)) {
this.retryNextServer = (Integer)requestConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer);
}
}
}
4.7 ribbon重试存在的问题:
issues: SocketTimeoutException(connect time out) is not judged as a ConnectionException: https://github.com/Netflix/ribbon/issues/359
csdn博客:https://blog.csdn.net/weixin_44032384/article/details/104414846
五、swagger的使用
5.1 分别在网关服务与其他服务中配置
(可以抽取swagger公共模块)
添加依赖
<!--swagger (已父项目中声明依赖与版本号:2.9.2) -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
网关服务中需要的配置:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Autowired
private DiscoveryClientRouteLocator discoveryClientRouteLocator;
@Primary
@Bean
public SwaggerResourcesProvider swaggerResourcesProvider() {
return new SwaggerResourcesProvider() {
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<Route> routes = discoveryClientRouteLocator.getRoutes();
for (Route route : routes) {
// 从网关中取获取各个服务的 api-docs
resources.add(swaggerResource(route.getId(), route.getFullPath().replace("**", "v2/api-docs")));
}
return resources;
}
};
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
其他微服务中需要的配置:
@Configuration
@EnableSwagger2
@ConfigurationProperties(prefix = "com.swagger") // 自定义前缀
@Data
public class Swagger2Config {
//在各个项目配置文件中添加相应的属性:
private String basePackage = "com.basePackage";
private String title = "swagger-title";
private String description;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select().apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any()).build().securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 文档标题
.title(title)
// 文档描述
.description(StringUtils.hasText(description) ? description : title)
// .termsOfServiceUrl("https://www.***.com")
// 设置文档版本号
.version("v2.3")
.build();
}
private List<ApiKey> securitySchemes() {
List<ApiKey> apiKeyList = new ArrayList();
apiKeyList.add(new ApiKey("Authorization", "Authorization", "header"));
return apiKeyList;
}
private List<SecurityContext> securityContexts() {
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(
SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("^(?!auth).*$"))
.build());
return securityContexts;
}
List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> securityReferences = new ArrayList<>();
securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
return securityReferences;
}
}
访问地址
ip:port/swagger-ui.html
可以通过网关服务的swagger页面去调用其他服务(会先经过网关)
如果是直接访问某个服务的swagger页面,则直接调用该服务,不经过网关
5.2 注解的使用
略
六、Maven在 Linux中使用
6.1 maven3.6.1下载地址
wget https://archive.apache.org/dist/maven/maven-3/3.6.1/binaries/
6.2 修改settings.xml配置
#自定义仓库地址
<localRepository>/usr/local/server/maven/repository3.6.1</localRepository>
#阿里云maven仓库
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
#修改maven的默认jdk版本为1.8
<profiles>
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>
</profiles>
6.3 配置mvn环境变量
echo "export MAVEN_HOME=/usr/local/maven3.6.1/apache-maven-3.6.1" >> /etc/profile
echo "export PATH=$MAVEN_HOME/bin:$PATH" >> /etc/profile
source /etc/profile
测试:
mvn -v
6.4 mvn常用命令
mvn clean
mvn compile # 编译
mvn package # 打包
mvn install # 打包并构建到本地仓库
mvn install -Dmaven.test.skip=true # 打包并跳过测试
#单独构建模块 commonweb 同时会构建commonweb模块依赖的其他模块(上游模块)
mvn install -pl commonweb -am
#单独构建模块commonweb ,同时构建依赖模块commonweb 的其他模块(下游模块)
mvn install -pl commonweb -amd
#单独构建模块nnhx-eureka 并跳过测试
mvn install -pl nnhx-eureka -am -Dmaven.test.skip=true
#单独构建模块register-center 下面的 nnhx-eureka 并跳过测试
mvn install -pl register-center/nnhx-eureka -am -Dmaven.test.skip=true
ps:构建指定模块时,建议先构建其上游模块(如果少量模块构建推荐使用命令操作,带上此参数【 -am】 ) ,且尽量避免同时构建同一个模块