hello云胜

技术与生活

0%

关于springcloud gateway的介绍闲言少叙,直接上手

快速上手

使用spring cloud gateway非常容易上手,在全面学习之前,我们先来看一个简单的小例子。

创建一个spring boot项目

使用spring initializr创建一个spring boot项目,选择webflux和gateway的依赖。

编写一个简单的路由

在application.yml中添加如下配置:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: path_route
predicates:
- Path=/get
uri: http://www.baidu.com

通过这个简单的配置文件,可以看出spring cloud gateway是由一些列route组成的

route路由是Spring Cloud Gateway的基本模块,它由一个ID、一个目标URI、一系列的断言和一系列的过滤器组成。

上面这个配置的含义通过断言对路径是/get的请求,转发到uri http://www.baidu.com。

启动项目

启动项目,访问[http://localhost:8080/get,可以看到页面加载了百度首页。

基本流程

熟悉Spring Cloud gateway要解决的问题和基本流程,才能让我们更好的学习它。spring cloud gateway的基本流程如下图所示:

img.png

当客户端发送请求到Spring Cloud Gateway时,spring gateway会根据请求的信息和路由规则匹配相应的Gateway Web Handler。Handler会通过一个filter的链路来对请求进行处理,filter可以在request代理发送前或者发送后进行数据处理。

核心概念

路由

路由是Spring Cloud Gateway的基本模块,它由一个ID、一个目标URI、一系列的断言和一系列的过滤器组成。 当一个请求进入Spring Cloud Gateway时,系统会按照顺序去试着匹配所有的路由,当匹配成功时,就会执行相应的过滤器链。然后转发给目标URI。

实际上使用Spring Cloud Gateway时,我们主要的工作就是指定路由规则。也就是指定路由的ID、目标URI、断言和过滤器。指定路由规则的方式有两种,一种是在配置文件中指定,另一种是通过编码的方式指定。

配置文件指定路由规则

在application.yml中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://www.baidu.com
predicates:
- Path=/get
- id: host_route
uri: http://www.baidu.com
predicates:
- Host=**.baidu.com

编码指定路由规则

在Spring Cloud Gateway中,我们可以通过编码的方式去指定路由规则,这种方式更加灵活,我们可以在运行时动态的添加、删除路由规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class GatewayConfig {

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://www.baidu.com"))
.route("host_route", r -> r.host("*.baidu.com")
.uri("http://www.baidu.com"))
.build();
}
}

无论是哪种方式,在路由的配置中,我们基本上需要做的就是指定断言、设定过滤器、指定跳转或者处理的handler。 下面我们就一一了解下这些概念。

断言

断言是一个布尔表达式,它的作用是判断请求是否满足路由的条件。 如果断言为true,则匹配该路由,否则不匹配。 断言可以通过配置文件或者编码的方式去指定。

配置文件指定断言

在配置文件中指定断言又有两种方式,一种是快捷方式实现,一种是全量方式实现。

快捷方式实现
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Cookie=mycookie,mycookievalue
全量方式实现
1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- name: Cookie
args:
name: mycookie
regexp: mycookievalue

通过上述两种方式,我们可以看到:快捷方式通过逗号分隔的方式去指定断言的名称和参数,全量方式通过name和args两个属性去指定断言的名称和参数。

编码指定断言

在编码中指定断言,我们可以通过PredicateSpec的方法去指定断言,如下:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class GatewayConfig {

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get").uri("http://www.baidu.com"))
.route("host_route", r -> r.host("*.baidu.com").uri("http://www.baidu.com"))
.build();
}
}

在上述例子中,我们通过编码的方式去指定了两个路由规则,分别是path_route和host_route。我们通过编码的方式去设定断言、过滤器和目标URI。

断言中的segment

在断言中可以使用segment进行占位。并且segment代表的值,可以作为变量用在下面会提到的GatewayFilter中。

举个例子。

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: path_route
uri: https://example.org
predicates:
- Path=/red/{segment},/blue/{segment}
filters:
- AddRequestHeader=X-Request-Red, Blue-{segment}

这个断言可以接收/red/1 或者/red/1/或者/red/blue或者/blue/green

如果matchTrailingSlash设置为false,/red/1/这种最后带/的就不能接收。matchTrailingSlash默认是true。

断言的总结

当你理解了断言的意图及配置方式后,使用断言就变得非常简单。一些常用的内置的断言工厂,你可以通过官网去学习他们的使用方法。

过滤器

过滤器是Spring Cloud Gateway的核心组件,它的作用是在请求被路由之前或之后对请求进行修改。Spring Cloud Gateway的过滤器分为两种类型:GatewayFilter和GlobalFilter

  • GlobalFilter:全局过滤器,可以在请求被路由之前或之后对请求进行修改。
  • GatewayFilter:作用于单个路由的局部过滤器,可以在请求被路由之前或之后对请求进行修改。

全局过滤器

全局过滤器是作用于所有路由的过滤器,可以在请求被路由之前或之后对请求进行修改。Spring Gateway内置了一些全局过滤器。除了内置的常用全局过滤器以外,我们还可以自定义全局过滤器。关于内置的全局过滤器,你可以通过官网去一一了解,或者有用到全局过滤器的时候先去官网查找。如果没有合适的再考虑自定义过滤器。

配置文件指定全局过滤器

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
globalfilters:
- name: AddRequestHeader
args:
name: X-Request-Global-Foo
value: Global-Bar

编码指定全局过滤器

1
2
3
4
5
6
7
8
9
10
@Configuration
public class GatewayConfig {
@Bean
public GlobalFilter customGlobalFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate().header("X-Request-Global-Foo", "Global-Bar").build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}

自定义全局过滤器

自定义过滤器需要实现GatewayFilter接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("global filter");
return chain.filter(exchange);
}

@Override
public int getOrder() {
return 0;
}
}

在实际的使用中可以根据自己的业务需求去使用或者组合使用合适的过滤器。

局部过滤器

局部过滤器是作用于单个路由的过滤器,可以在请求被路由之前或之后对请求进行修改。Spring Gateway内置了一些局部过滤器。除了内置的常用局部过滤器以外,我们还可以自定义局部过滤器。 关于内置的全局过滤器,你可以通过官网去一一了解,或者有用到全局过滤器的时候先去官网查找。如果没有合适的再考虑自定义过滤器。

配置文件中配置局部过滤器

在配置文件中,定义的Router中,我们通过Filters来指定过滤器。在Filters中,我们可以指定多个过滤器,过滤器的执行顺序是按照Filters中的顺序来执行的。在每个过滤器中,我们通过Name和Args来指定过滤器的名称和参数。或者我们可以通过 Name =Args的方式来设定过滤器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://www.baidu.com
predicates:
- Path=/get
filters:
- AddRequestHeader=X-Request-Foo, Bar
- AddResponseHeader=X-Response-Foo, Bar
- DedupeResponseHeader=Foo
- Hystrix=fooCommand
- PrefixPath=/httpbin
- PreserveHostHeader
- RemoveRequestHeader=Foo
- RemoveResponseHeader=Foo
- RewritePath=/foo/(?<segment>.*), /$\{segment}
- RewriteResponseHeader=Location, http://newlocation.org
- RequestRateLimiter=5, 1, SECONDS
- Retry=3
- SaveSession
- SecureHeaders
- SetPath=/foo/{segment}
- SetRequestHeader=X-Request-Foo, Bar
- SetResponseHeader=X-Response-Foo, Bar
- SetStatus=401
- StripPrefix=1
- StripRequestHeader=Foo
- StripResponseHeader=Foo
- Weight=foo, 5

编码使用局部过滤器

在编码中,我们可以通过Filters来指定过滤器。在Filters中,我们可以指定多个过滤器,过滤器的执行顺序是按照Filters中的顺序来执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class GatewayConfig {

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.filters(f -> f.addRequestHeader("X-Request-Foo", "Bar")
.addResponseHeader("X-Response-Foo", "Bar"))
.uri("http://www.baidu.com"))
.build();
}
}

自定义局部过滤器

自定义过滤器需要实现GatewayFilter接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class MyGatewayFilter implements GatewayFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("gateway filter");
return chain.filter(exchange);
}

@Override
public int getOrder() {
return 0;
}
}

Handler Function

通过配置断言和过滤器,可以让Spring Cloud Gateway将请求路由到指定的服务上。但是,如果我们想要在Spring Cloud Gateway中直接处理请求,而不是将请求路由到指定的服务上,那么我们就需要使用Handler Function。

Handler Function是WebFlux中的一个概念,它是一个函数,它接收一个ServerRequest对象,返回一个Mono对象。在Spring Cloud Gateway中,我们可以通过Handler Function来处理请求,而不是将请求路由到指定的服务上。

自定义一个Handler Function

自定义一个Handler Function需要实现HandlerFunction接口,如下:

1
2
3
4
5
6
7
8
@Component
public class MyHandlerFunction implements HandlerFunction<ServerResponse> {

@Override
public Mono<ServerResponse> handle(ServerRequest request) {
return ServerResponse.ok().body(BodyInserters.fromObject("hello world"));
}
}

使用Handler Function

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.beans.factory.annotation.Autowired;

@Configuration
public class GatewayConfig {
@Autowired
private MyHandlerFunction myHandlerFunction;
@Bean
public RouterFunction<ServerResponse> htmlRouterFunction() {
return RouterFunctions.route(RequestPredicates.path("/fallback"), myHandlerFunction));
}
}

总结

Spring Cloud Gateway在微服务中使用相当的简单方便。大多数使用,我们只需要配置Spring Cloud Gateway连接到注册中心,然后配置路由规则即可。甚至很多时候,路由配置都不需要设计,他会默认把请求和注册中心的服务进行匹配,如果匹配到,就会自动路由到对应的服务上。

image-20200325212208043

重新打包,默认是dev环境

image-20200325212541241

想想也对,本地启动应该是找最近编译出的代码。因为我本地刚好打了个生产包,所以会启动生产环境的配置。

如果要指定环境,idea里应该是在

image-20200325213138666

这里配置。

这样才是比较好的习惯

Springboot下的WebSocket开发

今天遇到一个需求,需要对接第三方扫码跳转。一种方案是前端页面轮询后端服务,但是这种空轮询会虚耗资源,而且也不优雅。所以决定使用另一种方案,websocket。以前就知道websocket,属于长连接,非常适合这种场景的需求。但是一直没机会用,今天正好可以使用一下。

简单记录一下步骤,亲测可用。

  1. 引入依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

springboot已经非常贴心的为我们编写好了starter

  1. 配置config
1
2
3
4
5
6
7
8
9
@Configuration
public class WebSocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}

}

​ 必须有这个config,把ws服务暴露出去。

  1. 编写webSocket server

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    @Slf4j
    @Component
    @ServerEndpoint("/webSocket/{id}")
    public class MyWebSocket {
    /**
    * 静态变量 用来记录当前在线连接数
    */
    private static int onlineCount = 0;

    /**
    * 服务端与单一客户端通信 使用Map来存放 其中标识Key为id
    */
    private static ConcurrentMap<String, MyWebSocket> webSocketMap = new ConcurrentHashMap<>();
    //不需要区分可使用set
    //private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>();

    public static ConcurrentMap<String, MyWebSocket> getWebSocketMap() {
    return webSocketMap;
    }

    /**
    * 与某个客户端的连接会话 需要通过它来给客户端发送数据
    */
    private Session session;

    /**
    * 连接建立成功调用的方法
    *
    * @param session 可选的参数 session为与某个客户端的连接会话 需要通过它来给客户端发送数据
    */
    @OnOpen
    public void onOpen(Session session, @PathParam("id") String id) {
    this.session = session;

    webSocketMap.put(id, this);

    addOnlineCount();
    log.info("有新连接加入,当前在线数为" + getOnlineCount());
    }

    /**
    * 连接关闭调用的方法
    */
    @OnClose
    public void onClose() {
    Map<String, String> map = session.getPathParameters();
    webSocketMap.remove(Integer.parseInt(map.get("id")));

    subOnlineCount();
    log.info("有一连接关闭!当前在线数为" + getOnlineCount());
    }

    /**
    * 收到客户端消息后调用的方法
    *
    * @param message 客户端发送过来的消息
    * @param session 可选的参数
    */
    @OnMessage
    public void onMessage(String message, Session session) {
    log.info("来自客户端 " + session.getId() + " 的消息:" + message);
    }

    /**
    * 发生错误时调用
    *
    * @param session
    * @param error
    */
    @OnError
    public void onError(Session session, Throwable error) {
    log.error("LoginResultWebSocket 发生错误");
    error.printStackTrace();
    }

    /**
    * 发送消息
    *
    * @param message
    * @throws IOException
    */
    public void sendMessage(String message) throws IOException {
    this.session.getBasicRemote().sendText(message);
    }

    public static synchronized int getOnlineCount() {
    return onlineCount;
    }

    public static synchronized void addOnlineCount() {
    MyWebSocket.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
    MyWebSocket.onlineCount--;
    }

    }
  2. 编写测试页面

    页面代码就不贴了,网上很多,需要的可以看github,地址在文章最后。

    就是一个按钮打开socket链接。一个按钮向后端发送消息。一个用来展示从服务端收到的消息的p标签。

    image-20200305212917566

点击发送消息后,

image-20200305213152016

可以看到,server端已经接收到。

然后测试,页面也可以接收到消息。我写了一个请求,触发一下sendMessage方法。

image-20200305213237513

总结:websocket入门使用还是很简单的,也很有趣。可以用来写个在线聊天室demo。

springboot中使用redis的发布订阅

redis的发布订阅模式

通常我们只把redis当缓存数据库来使用。但是redis从5版本开始增加了一个消息的发布订阅功能。

可以类比于一个简化版的消息队列。当然功能弱很多。之前我一直认为这个功能很鸡肋。直到最近负责一个项目,需要一个消息订阅的功能。如果再去搭建一个rocketmq感觉浪费服务器。使用redis的发布订阅也能解决问题。所以是我以前狭隘了,存在即合理。

下面简单介绍下redis的发布订阅

订阅频道

首先,客户端使用SUBSCRIBE命令来订阅一个或多个频道。例如,以下命令订阅了名为news的频道:

1
SUBSCRIBE news

客户端也可以同时订阅多个频道,例如:

1
SUBSCRIBE news sport weather

发布消息

使用PUBLISH命令可以向指定的频道发布一条消息。当消息发布到某个频道时,所有订阅该频道的客户端都会收到通知。(注意:所有订阅者都会收到)。例如,以下命令将消息hello world发布到名为news的频道:

1
PUBLISH news "hello world"

之前的订阅者客户端会显示

1
2
3
1) "message"
2) "news"
3) "hello world"

退订频道

如果客户端不再需要订阅某个频道,可以使用UNSUBSCRIBE命令来退订。例如,以下命令退订了名为news的频道:

1
UNSUBSCRIBE news

客户端也可以同时退订多个频道,例如:

1
UNSUBSCRIBE news sport weather

模式匹配订阅

除了订阅具体的频道外,客户端还可以使用通配符订阅一类频道。通配符*可以匹配任意字符串,而?则可以匹配单个字符。例如,以下命令订阅所有以news.为前缀的频道:

1
PSUBSCRIBE news.*

模式匹配退订

如果客户端不再需要订阅某类频道,可以使用PUNSUBSCRIBE命令来模式匹配退订。例如,以下命令退订所有以news.为前缀的频道:

1
PUNSUBSCRIBE news.*

好了,下面在代码里使用

springboot中使用redis的发布订阅

添加Redis依赖

首先,您需要在您的Spring Boot项目中添加Redis依赖。可以使用以下Maven依赖项:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis连接

接下来,需要配置Redis连接。在application.yml文件中添加以下内容:

1
2
3
4
5
6
spring:
redis:
cluster:
nodes: xxxx;xxxx;我用的是redis集群
max-redirects: 3
password: xxxxxxx

订阅频道

要订阅一个频道,需要在RedisConfig配置类中实例化一个RedisMessageListenerContainer类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class RedisConfig {

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MyMessageListener listener) {

RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listener, new ChannelTopic("news"));

return container;
}
// 其他redis的配置代码,略
}

这里我们订阅了名为news的一个channel。

代码中MyMessageListener是自定义的listener,用于处理订阅接收到的消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class PathAuthRedisMessageListener implements MessageListener {

@Autowired
private RedisTemplate redisTemplate;

@Override
public void onMessage(Message message, byte[] pattern) {
// 获取消息
byte[] messageBody = message.getBody();
// 使用值序列化器转换
Object msg = redisTemplate.getValueSerializer().deserialize(messageBody);
// 获取监听的频道
byte[] channelByte = message.getChannel();
// 使用字符串序列化器转换
Object channel = redisTemplate.getStringSerializer().deserialize(channelByte);
// 渠道名称转换
String patternStr = new String(pattern);
System.out.println(patternStr);
System.out.println("---频道---: " + channel);
System.out.println("---消息内容---: " + msg);
}
}

发布消息

要发布一条消息,也很简单。示例:

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class MessageController {

@Autowired
private RedisTemplate template;

@PostMapping("/message")
public void sendMessage(@RequestParam String message) {
template.convertAndSend("news", message);
}
}

这样就可以将消息发布到news频道。

总结

总体来说,springboot里集成redis的发布订阅功能还是比较简单的。受限于redis发布订阅本身能力的限制,毕竟Redis不是专门做发布订阅的,比如redis的发布订阅不支持消息的持久化,消息丢了就是丢了。所以只适合在某些简单并且不重要的业务场景下使用。

接入SpringbootAdmin

1,SpringbootAdmin是什么

Spring Boot Admin 是一个管理和监控 Spring Boot 应用程序的开源软件,它是在 Spring Boot Actuator 的基础上提供简洁的可视化 WEB UI。个人认为Spring Boot Actuator 是Springboot体系中非常好用且强大的监控能力节点,极大的方便了我们对springboot应用进行业务监控。但是,Spring Boot Actuator 监控接口返回的都是json数据,需要我们进一步开发自己的监控系统。Spring Boot Admin 就是这样一个系统。

Spring Boot Admin并不是 Spring Boot 官方出品的开源软件,但是其软件质量和使用广泛度都非常的高,并且 Spring Boot Admin 会及时随着 Spring Boot 的更新而更新,当 Spring Boot 推出 2.X 版本时 Spring Boot Admin 也及时进行了更新。

它可以在列表中浏览所有被监控 spring-boot 项目的基本信息、详细的 Health 信息、内存信息、JVM 信息、垃圾回收信息、各种配置信息(比如数据源、缓存列表和命中率)等,还可以直接修改 logger 的 level。

2,接入步骤

Spring Boot Admin 分为两部分。server端和client端。我们要做的就是部署server端,再我们的应用中集成client端。整个接入过程不算复杂,看完这篇文章,你一定会。

部署Server端

新建一个普通的springboot工程。

引入以下依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.2.2</version>
</dependency>

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

配置一个服务端口

1
server.port=8000

EnableAdminServer

1
2
3
4
5
6
7
8
9
@EnableAdminServer
@SpringBootApplication
public class MonitorApplication {

public static void main(String[] args) {
SpringApplication.run(MonitorApplication.class, args);
}

}

好了,server端配置完成。部署启动即可。

image-20200417161404855

监控client端

添加依赖:

配置文件配置

1
spring.boot.admin.client.url=http://localhost:8000 

指定server地址

1
management.endpoints.web.exposure.include=* 

暴露可被监控的服务节点。这里为了演示,开放了所有,具体的应该根据自己的需要配置。

image-20200418103058522

image-20200418103121077

3,开启认证

上面的步骤已经完成对应用的监控。但是有个问题,没有权限管理,任何人都可以随便查看甚至操作应用。这在生产环境是绝对不允许的,所以,我们必须开启权限认证。

server端

引入springboot-security

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置登录密码

1
2
3
4
5
spring:
security:
user:
name: "admin"
password: "admin"

main类增加安全配置认证路由代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@EnableAdminServer
@SpringBootApplication
public class MonitorApplication {

public static void main(String[] args) {
SpringApplication.run(MonitorApplication.class, args);
}

@Configuration
public static class SecuritySecureConfig extends WebSecurityConfigurerAdapter {
private final String adminContextPath;

public SecuritySecureConfig(AdminServerProperties adminServerProperties) {
this.adminContextPath = adminServerProperties.getContextPath();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");

http.authorizeRequests()
.antMatchers(adminContextPath + "/assets/**").permitAll()
.antMatchers(adminContextPath + "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage(adminContextPath + "/login").successHandler(successHandler).and()
.logout().logoutUrl(adminContextPath + "/logout").and()
.httpBasic().and()
.csrf().disable();
}
}

server端好了

client端改造

因为现在server端启用了密码,所以client端是注册不上来的,需要配置密码

1
2
3
4
5
6
boot:
admin:
client:
url: http://localhost:8000
username: admin
password: admin

image-20200418104224993

现在需要使用用户名密码登录,并且有了退出等基本功能

image-20200418104206617

MongoTemplate进行分页查询

之前使用mysql做数据库,配合mybatis-plus进行开发,整体感觉还是很流畅的。

但是遇到表结构变更,需要加减字段的时候就有些麻烦,当然这不是mybatis-plus的问题。

Springboot配置Https

主要说下我在配置中踩到的一个坑。

我这里有一个老的应用,之前部署在tomcat下,现在用springboot进行了重构。现在直接使用jar包部署,所以需要把之前配置在tomcat中的ssl配置一直到springboot的配置文件下。

1,找到ssl文件

之前的tomcat中的配置ssl是在server.xml中,找到对应的配置代码

1
2
3
4
5
6
7
8
9
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/xxx.com.jks"
certificateKeystorePassword="我的密码"
certificateKeyAlias="xxx.com"
type="RSA" />
</SSLHostConfig>
</Connector>

顺藤摸瓜的找到jks文件,下载下来

2,springboot中的配置

springboot中的配置也很简单

1
2
3
4
5
server:
ssl:
key-store-type: JKS
key-store: classpath:xxx.com.jks
key-store-password: 我的密码

key-store指的是jks文件的放置路径,我这里是直接放在resources下面

image-20200523165732766

3,启动

这样就算配置完了,简单吧,然后启动。华丽丽的报错了。棒棒的。

我们看下报错:

1
Caused by: java.lang.IllegalArgumentException: Invalid keystore format

说我们的文件格式不对。

但是我的这个文件在旧的网站用的好好的,经过比对,我确定文件是没问题的。

后来发现,我的原始jks文件是没有问题,但是经过maven打包之后的jks文件被改动了。

image-20200522173820333

这是原始文件

image-20200522174220326

这是打包中的文件,

可以看到,变成了10k。大小都变了。看来maven真的做了什么操作

4,找到原因

我的项目里,使用了maven打包时,进行多环境配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<resources>
<resource>
<directory>src/main/resources</directory>
<!-- 处理文件时替换文件中的变量 -->
<filtering>true</filtering>
<excludes>
<!-- 打包时排除文件,可自行添加test.yml -->
<exclude>application.yml</exclude>
<exclude>application-dev.yml</exclude>
<exclude>application-prod.yml</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<!-- 打包时所包含得文件 -->
<includes>
<include>application.yml</include>
<include>application-${profileActive}.yml</include>
</includes>
</resource>
</resources>

我这里设置的目录是src/main/resources。所以resources下面的所有文件会被maven打开,并按照规则做一些改动。导致了我的jks文件被破坏。

5,解决

改一下maven配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<resources>
<resource>
<directory>src/main/resources</directory>
<!-- 处理文件时替换文件中的变量 -->
<filtering>true</filtering>
<excludes>
<!-- 打包时排除文件,可自行添加test.yml -->
<exclude>application.yml</exclude>
<exclude>application-dev.yml</exclude>
<exclude>application-prod.yml</exclude>
<exclude>*.jks</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<!-- 打包时所包含得文件 -->
<includes>
<include>application.yml</include>
<include>application-${profileActive}.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>*.jks</include>
</includes>
</resource>
</resources>

加上一个,打包时不要处理jks文件的配置

解决。

总结

回头想想,这个坑应该可以很快被识别出。因为maven打包后会修改我的yml配置文件。那么按照之前的配置,也会修改我的其他文件。还是对maven打包的理解不深,那么以后要注意,如果加入其他文件到resources下也可能出现这个问题。

定制python3.11.5镜像

之前制作了python3.6.5的Dockerfile。

现在业务需要3.11的python镜像,本以为只需要升级下python版本即可。

但是在实际解决的过程中需要很多问题。3.7以上的python有一些升级问题需要处理。

image-20231013182859157

1
WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available

没有ssl模块。

经过查询得知,是openssl版本太低,需要升级到1.1.x

实际上解决openssl之后,在部署应用时又遇到了各种依赖缺失的问题

只能一一解决。(你可能会遇到自己应用的特定依赖缺失,需要你自己修改了)

最后成功的dockerfile如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
FROM centos:7.5.1804
MAINTAINER uncley

ENV PATH $PATH:/usr/local/python3/bin/
ENV PYTHONIOENCODING utf-8

RUN set -ex \
# 替换yum源
&& mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup \
&& curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo \
&& sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo \
# 安装python依赖库
&& yum makecache \
&& yum -y install perl libffi-devel zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make wget \
&& yum clean all \
# 安装openssl1.1.1d
&& wget https://github.com/openssl/openssl/archive/OpenSSL_1_1_1d.tar.gz
&& tar -zxvf OpenSSL_1_1_1d.tar.gz && cd openssl-OpenSSL_1_1_1d && ./config --prefix=/usr/local/openssl
&& make && make install
&& mv /usr/lib64/libssl.so /usr/lib64/libssl.so.old && ln -s /usr/local/openssl/bin/openssl /usr/bin/openssl && ln -s /usr/local/openssl/include/openssl /usr/include/openssl && ln -s /usr/local/openssl/lib/libssl.so /usr/lib64/libssl.so && echo "/usr/local/openssl/lib" >> /etc/ld.so.conf && ldconfig -v
&& rm -rf /var/cache/yum \
# 下载安装python3
&& wget https://www.python.org/ftp/python/3.11.5/Python-3.11.5.tgz \
&& mkdir -p /usr/local/python3 \
&& tar -zxvf Python-3.11.5.tgz \
&& cd Python-3.11.5 \
&& ./configure --prefix=/usr/local/python3 --with-openssl=/usr/local/openssl \
&& make && make install && make clean \
# 修改pip默认镜像源
&& mkdir -p ~/.pip \
&& echo '[global]' > ~/.pip/pip.conf \
&& echo 'index-url = https://pypi.tuna.tsinghua.edu.cn/simple' >> ~/.pip/pip.conf \
&& echo 'trusted-host = pypi.tuna.tsinghua.edu.cn' >> ~/.pip/pip.conf \
&& echo 'timeout = 120' >> ~/.pip/pip.conf \
# 更新pip
&& pip3 install --upgrade pip \
# 安装wheel
&& pip3 install wheel \
# 删除安装包
&& cd .. \
&& rm -rf /Python* \
&& find / -name "*.py[co]" -exec rm '{}' ';' \
# 设置系统时区
&& rm -rf /etc/localtime \
&& ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

定制基础Python镜像

虽说python基础镜像网上很多,但是每次换一个基础镜像就需要仔细分析一下他的文件结构,而且很怕踩坑。所以决定定制一个自己的基础镜像,这样各方面都比较有把握。

基于centos7.5的python镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
FROM centos:7.5.1804
MAINTAINER uncleY <uncleY@qq.com>

ENV PATH $PATH:/usr/local/python3/bin/
ENV PYTHONIOENCODING utf-8

RUN set -ex \
# 替换yum源
&& mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup \
&& curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo \
&& sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo \
# 安装python依赖库
&& yum makecache \
&& yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make wget \
&& yum clean all \
&& rm -rf /var/cache/yum \
# 下载安装python3
&& wget https://www.python.org/ftp/python/3.6.4/Python-3.6.4.tgz \
&& mkdir -p /usr/local/python3 \
&& tar -zxvf Python-3.6.4.tgz \
&& cd Python-3.6.4 \
&& ./configure --prefix=/usr/local/python3 \
&& make && make install && make clean \
# 修改pip默认镜像源
&& mkdir -p ~/.pip \
&& echo '[global]' > ~/.pip/pip.conf \
&& echo 'index-url = https://pypi.tuna.tsinghua.edu.cn/simple' >> ~/.pip/pip.conf \
&& echo 'trusted-host = pypi.tuna.tsinghua.edu.cn' >> ~/.pip/pip.conf \
&& echo 'timeout = 120' >> ~/.pip/pip.conf \
# 更新pip
&& pip3 install --upgrade pip \
# 安装wheel
&& pip3 install wheel \
# 删除安装包
&& cd .. \
&& rm -rf /Python* \
&& find / -name "*.py[co]" -exec rm '{}' ';' \
# 设置系统时区
&& rm -rf /etc/localtime \
&& ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

我已经验证过了,脚本可行。

image-20230906153448942

最终生成镜像大小:454MB,感觉还好。虽然alpine镜像很小,只有90M。但是很坑,用过的都知道,要排查问题的时候要啥啥没有。

Alpine的致命问题:标准的Linux安装包在Alpine Linux上根本无法使用。

大多数Linux发行版都使用GNU版本的标准C库(glibc),几乎所有基于C语言的脚本语言都需要这个库,包括Python。但Alpine Linux使用的是musl,那些二进制安装包是针对glibc编译的,因此Alpine禁用了Linux安装包支持。现在大多数Python包都在PyPI上包含了二进制安装包,大大加快了安装时间。但是如果你使用的是Alpine Linux,你需要编译你使用的每一个Python包中的所有C源码。

Alpine的线程默认堆栈容量较小,这会导致Python崩溃,同时也会影响python应用的运行速度。

使用

然后我们就可以基于此基础镜像,再打我们的代码

1
2
3
4
5
FROM harbor-test.xxx.net/dts/python:3.6.4
COPY src /src/
RUN pip3 install -r /src/requirements.txt
EXPOSE 5000
ENTRYPOINT [ "python3", "/src/app.py" ]

关于制作镜像的建议:

0、set -ex,作用是当命令发生错误的时候,停止脚本的执行

1、RUN中尽量使用 \ &&连接命令的方式,减少镜像层数,可以一定程度减少体积。

2、尽可能删除不需要的文件,也是为了减少镜像体积。

3、Python默认不安装wheel,但是第三方库常需要使用wheel安装,所以加上它。如果你不需要,可以删掉。

4、docker运行时程序获取系统时间时,如打印日志等,获取的是docker镜像内文件系统的时区设置,默认是格林尼治标准时区,所以需要设置为所在的时区。

源码编译安装nginx

Nginx 源码包安装步骤相比其他安装方法比较繁琐,但是操作不复杂,需要提前安装一些 Nginx 依赖库。

依赖库安装

  1. 安装gcc环境

编译时依赖gcc环境

1
yum -y install gcc gcc-c++ autoconf automake make
  1. 安装 pcre

提供nginx支持重写功能

1
yum -y install pcre pcre-devel
  1. 安装zlib

zlib 库提供了很多压缩和解压缩的方式,nginx 使用 zlib 对 http 包内容进行 gzip 压缩

1
yum -y install zlib zlib-devel make libtool
  1. 安装openssl

安全套接字层密码库,用于通信加密

1
yum -y install openssl openssl-devel

合计

1
yum -y install gcc gcc-c++ autoconf automake make pcre pcre-devel zlib zlib-devel make libtool openssl openssl-devel

nginx 安装

手动创建用户和用户组

1
2
groupadd nginx
useradd nginx -g nginx -s /sbin/nologin -M

官网下载nginx源码包

1
wget https://nginx.org/download/nginx-1.24.0.tar.gz

解压

1
tar -zxvf nginx-1.24.0.tar.gz && cd nginx-1.24.0

检查平台安装环境

1
./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --user=nginx --group=nginx

参数说明:

–prefix=/usr/local/nginx : 编译安装目录

–user=nginx : 所属用户nginx

–group=nginx : 所属组nginx

–with-http_stub_status_module : 该模块提供nginx的基本状态信息

–with-http_ssl_module : 支持HTTPS

编译源码并安装

1
make && make install

nginx编译安装完成以后,修改/usr/local/nginx/conf/nginx.conf

1
user nginx nginx;

ningx操作

启动服务

1
/usr/local/nginx/sbin/nginx

重新加载服务

1
/usr/local/nginx/sbin/nginx -s reload

停止服务

1
/usr/local/nginx/sbin/nginx -s stop

html的目录

1
/usr/local/nginx/html

conf目录

1
/usr/local/nginx/conf