spring session(1)

前言

在开始spring-session揭秘之前,先做下热脑(活动活动脑子)运动。主要从以下三个方面进行热脑:

  • 为什么要spring-session
  • 比较traditional-session方案和spring-session方案
  • JSR340规范与spring-session的透明继承

    一.为什么要spring-session

    在传统单机web应用中,一般使用tomcat/jetty等web容器时,用户的session都是由容器管理。浏览器使用cookie中记录sessionId,容器根据sessionId判断用户是否存在会话session。这里的限制是,session存储在web容器中,被单台服务器容器管理。

但是网站主键演变,分布式应用和集群是趋势(提高性能)。此时用户的请求可能被负载分发至不同的服务器,此时传统的web容器管理用户会话session的方式即行不通。除非集群或者分布式web应用能够共享session,尽管tomcat等支持这样做。但是这样存在以下两点问题:

  • 需要侵入web容器,提高问题的复杂
  • web容器之间共享session,集群机器之间势必要交互耦合
    基于这些,必须提供新的可靠的集群分布式/集群session的解决方案,突破traditional-session单机限制(即web容器session方式,下面简称traditional-session),spring-session应用而生。

二.比较traditional-session方案和spring-session方案

下图展示了traditional-session和spring-session的区别

logo
传统模式中,当request进入web容器,根据reqest获取session时,如果web容器中存在session则返回,如果不存在,web容器则创建一个session。然后返回response时,将sessonId作为response的head一并返回给客户端或者浏览器。

但是上节中说明了traditional-session的局限性在于:单机session。在此限制的相反面,即将session从web容器中抽出来,形成独立的模块,以便分布式应用或者集群都能共享,即能解决。

spring-session的核心思想在于此:将session从web容器中剥离,存储在独立的存储服务器中。目前支持多种形式的session存储器:Redis、Database、MogonDB等。session的管理责任委托给spring-session承担。当request进入web容器,根据request获取session时,由spring-session负责存存储器中获取session,如果存在则返回,如果不存在则创建并持久化至存储器中。

JSR340规范与spring-session的透明继承

JSR340是Java Servlet 3.1的规范提案,其中定义了大量的api,包括:servlet、servletRequest/HttpServletRequest/HttpServletRequestWrapper、servletResponse/HttpServletResponse/HttpServletResponseWrapper、Filter、Session等,是标准的web容器需要遵循的规约,如tomcat/jetty/weblogic等等。

在日常的应用开发中,develpers也在频繁的使用servlet-api,比如:

以下的方式获取请求的session:

1
2
HttpServletRequest request = ...
HttpSession session = request.getSession(false);

其中HttpServletRequest和HttpSession都是servlet规范中定义的接口,web容器实现的标准。那如果引入spring-session,要如何获取session?

  • 遵循servlet规范,同样方式获取session,对应用代码无侵入且对于developers透明化
  • 全新实现一套session规范,定义一套新的api和session管理机制
    两种方案都可以实现,但是显然第一种更友好,且具有兼容性。spring-session正是第一种方案的实现。

实现第一种方案的关键点在于做到透明和兼容

接口适配:仍然使用HttpServletRequest获取session,获取到的session仍然是HttpSession类型——适配器模式
类型包装增强:Session不能存储在web容器内,要外化存储——装饰模式
让人兴奋的是,以上的需求在Servlet规范中的扩展性都是予以支持!Servlet规范中定义一系列的接口都是支持扩展,同时提供Filter支撑扩展点。

spring session入门

主要从以下两个方面来说spring-session:

  • 特点
  • 工作原理

    一.特点

    spring-session在无需绑定web容器的情况下提供对集群session的支持。并提供对以下情况的透明集成:

  • HttpSession:容许替换web容器的HttpSession

  • WebSocket:使用WebSocket通信时,提供Session的活跃
  • WebSession:容许以应用中立的方式替换webflux的webSession

下面以项目接入spring session redis为例

spring 项目

引入spring session的jar
代码:https://github.com/smallwenzi/testSpringSession

引入:

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
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.3.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.3.0.RELEASE</version>
</dependency>

<!-- Spring session redis 依赖start -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.6.0.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

配置redis

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.hostName}" />
<property name="port" value="${redis.port}" />
<property name="password" value="${redis.password}" />
<property name="usePool" value="${redis.usePool}" />
<property name="timeout" value="${redis.timeout}" />
</bean>

redis.properties(自行更改redis 配置)

1
2
3
4
5
redis.hostName=localhost
redis.port=6379
redis.password=
redis.usePool=true
redis.timeout=10000

开启spring session

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
@Configuration
//redisNamespace区分每个项目spring session key唯一
@EnableRedisHttpSession(redisNamespace = "testSpringSession")
public class SpringSessionHttpConfig {

private static final Logger logger = LoggerFactory.getLogger(SpringSessionHttpConfig.class);


@Value("${redis.taskexecutor.corepoolsize}")
private String corepoolsize;

@Value("${redis.taskexecutor.maxpoolsize}")
private String maxpoolsize;

@Value("${redis.taskexecutor.keepaliveseconds}")
private String keepaliveseconds;

@Value("${redis.taskexecutor.queuecapacity}")
private String queuecapacity;

@Value("${redis.taskexecutor.threadnameprefix}")
private String threadnameprefix;

//控制springsession 线程池 否则线程会无限制创建 导致oom 线程配置详细看看项目里test.properties
@Bean
public ThreadPoolTaskExecutor springSessionRedisTaskExecutor() {
logger.info("JedisPool注入成功!!");
ThreadPoolTaskExecutor springSessionRedisTaskExecutor = new ThreadPoolTaskExecutor();
springSessionRedisTaskExecutor.setCorePoolSize(getRedisTaskexecutorStrToInt(this.corepoolsize, 16));
springSessionRedisTaskExecutor.setMaxPoolSize(getRedisTaskexecutorStrToInt(this.maxpoolsize, 300));
springSessionRedisTaskExecutor.setKeepAliveSeconds(getRedisTaskexecutorStrToInt(this.keepaliveseconds, 30));
springSessionRedisTaskExecutor.setQueueCapacity(getRedisTaskexecutorStrToInt(this.queuecapacity, 500));
springSessionRedisTaskExecutor.setThreadNamePrefix(this.threadnameprefix);
return springSessionRedisTaskExecutor;
}

private int getRedisTaskexecutorStrToInt(String size, int defaultSize) {
try {
int sizeInt = Integer.parseInt(size);
return sizeInt;
} catch (Exception e) {
return defaultSize;
}
}

web.xml

1
2
3
4
5
6
7
8
9
10
 <filter>
<!-- 必须这样命名 SpringHttpSessionConfiguration类中定义了springSessionRepositoryFilter类 -->
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<!-- 需要spring session的访问路径 -->
<url-pattern>/*</url-pattern>
</filter-mapping>

本地分别部署tomcat 进行测试http://localhost:8082/testSpringSession/testSession.do
http://localhost:8080/testSpringSession/testSession.do
输出的session id是否一致
若一致则spring session成功

spring boot项目

项目:https://github.com/smallwenzi/testSpringBootSession

引入jar

1
2
3
4
5
6
7
8
9
          <dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>

开启springsession

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
//redisNamespace区分每个项目spring session key唯一
@EnableRedisHttpSession(redisNamespace = "testSpringBootSession")
public class SpringSessionHttpConfig {
private static final Logger logger = LoggerFactory.getLogger(SpringSessionHttpConfig.class);
@Value("${redis.taskexecutor.corepoolsize}")
private String corepoolsize;

@Value("${redis.taskexecutor.maxpoolsize}")
private String maxpoolsize;

@Value("${redis.taskexecutor.keepaliveseconds}")
private String keepaliveseconds;

@Value("${redis.taskexecutor.queuecapacity}")
private String queuecapacity;

@Value("${redis.taskexecutor.threadnameprefix}")
private String threadnameprefix;

//控制springsession 线程池 否则线程会无限制创建 导致oom 线程配置详细看看项目里application.properties
@Bean
public ThreadPoolTaskExecutor springSessionRedisTaskExecutor() {
logger.info("JedisPool注入成功!!");
ThreadPoolTaskExecutor springSessionRedisTaskExecutor = new ThreadPoolTaskExecutor();
springSessionRedisTaskExecutor.setCorePoolSize(getRedisTaskexecutorStrToInt(this.corepoolsize, 16));
springSessionRedisTaskExecutor.setMaxPoolSize(getRedisTaskexecutorStrToInt(this.maxpoolsize, 300));
springSessionRedisTaskExecutor.setKeepAliveSeconds(getRedisTaskexecutorStrToInt(this.keepaliveseconds, 30));
springSessionRedisTaskExecutor.setQueueCapacity(getRedisTaskexecutorStrToInt(this.queuecapacity, 500));
springSessionRedisTaskExecutor.setThreadNamePrefix(this.threadnameprefix);
return springSessionRedisTaskExecutor;
}

private int getRedisTaskexecutorStrToInt(String size, int defaultSize) {
try {
int sizeInt = Integer.parseInt(size);
return sizeInt;
} catch (Exception e) {
return defaultSize;
}
}
}

application.properties

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
#本地测试设置随机端口 (自行更改redis 配置)
server.port=0
# redis start
#spring.redis.database=0
spring.redis.password=
spring.redis.pool.max-idle=10
spring.redis.pool.min-idle=0
spring.redis.pool.max-active=100
spring.redis.pool.max-wait=1000
spring.redis.database=0
# Redis\u670D\u52A1\u5668\u5730\u5740
spring.redis.host=localhost
# Redis\u670D\u52A1\u5668\u8FDE\u63A5\u7AEF\u53E3
spring.redis.port=6379

# redis end
#spring session\u4f7f\u7528\u5b58\u50a8\u7c7b\u578b
spring.session.store-type=redis

##spring-session redis \u7ebf\u7a0b\u6c60\u914d\u7f6e
redis.taskexecutor.corepoolsize=16
redis.taskexecutor.maxpoolsize=300
redis.taskexecutor.keepaliveseconds=30
redis.taskexecutor.queuecapacity=500
redis.taskexecutor.threadnameprefix=testSpringBootSession-spring-session-redis-executor-thread:

本地分别启动 WebApplication进行测试http://localhost:port/testSession
http://localhost:port/
testSession
输出的session id是否一致
若一致则spring session成功

有兴趣可以读《spring sesson源码》

[Java] 查看占用 CPU 最高的线程

步骤

  • top 命令找出应用 pid-app
  • top -Hp 命令找出线程 pid-thread
  • printf ‘%x\n’ 命令将线程 pid 转换成 16 进制 pid-thread-hex
  • jstack | grep ‘pid-thread-hex’ 命令查看线程信息

    示例

  • 1.top 命令找出应用 pid-app
    logo

应用 pid 为8790

  • top -Hp 命令找出线程 pid-thread
    logo

线程 pid 为9702

  • printf ‘%x\n’ 命令将线程 pid 转换成 16 进制 pid-thread-hex
    logo

  • jstack | grep ‘pid-thread-hex’ 命令查看线程信息

logo

  • 查看整个JVM内存状态
    jmap -heap [pid]
    logo

  • 导出整个JVM 中内存信息,可以利用其它工具打开dump文件分析,例如jdk自带的visualvm工具
    jmap -dump:file=文件名.dump [pid]

使用MemoryAnalyzer.exe 分析dump
logo

  • 打印java线程数
    jcmd pid Thread.print> thread.txt
    logo

spring config bus

config pom.xml

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>

/configuration/src/main/resources/application-native.properties

1
2
3
4
5
6
7
8
9
spring.cloud.bus.enabled=true
spring.cloud.bus.trace.enabled=true
spring.rabbitmq.addresses=${RABBITMQ_ADDRESS}
spring.rabbitmq.username=${RABBITMQ_USERNAME}
spring.rabbitmq.password=${RABBITMQ_PASSWORD}
spring.rabbitmq.virtual-host=${RABBITMQ_VIRTUAL_HOST}
## \u5237\u65B0\u65F6\uFF0C\u5173\u95ED\u5B89\u5168\u9A8C\u8BC1
management.security.enabled=false
management.endpoints.web.exposure.include=bus-refresh

刷新链接:
http://ip:6081/actuator/bus-refresh

客户端
pom.xml

1
2
3
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency>

HelloController

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RefreshScope
// 使用该注解的类,会在接到SpringCloud配置中心配置刷新的时候,自动将新的配置更新到该类对应的字段中。
class HelloController {

@Value("${business.wx.appid}")
private String hello;

@RequestMapping("/hello")
public String from() {
return this.hello;
}
}

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
######## bus-amqp
## 刷新时,关闭安全验证
management.security.enabled=false
management.endpoints.web.exposure.include=bus-refresh
## 开启消息跟踪
spring.cloud.bus.enabled=true
spring.cloud.bus.trace.enabled=true
#mq的地址
spring.rabbitmq.addresses=
#mq的用户名
spring.rabbitmq.username=
#mq的密码
spring.rabbitmq.password=
spring.rabbitmq.publisherConfirms=false
spring.rabbitmq.publisherReturns=false
spring.rabbitmq.virtual-host=/

测试连接
测试入口:http://ip:port/hello

spring admin配置

spring admin 接入 eureka

pom.xml

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
<?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.test.springcloud</groupId>
<artifactId>springamdinserver</artifactId>
<version>2.1</version>

<name>基础设施:springamdinserver</name>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
</parent>

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

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>

<plugins>
<!-- 源码格式 -->
<plugin>
<groupId>com.googlecode.maven-java-formatter-plugin</groupId>
<artifactId>maven-java-formatter-plugin</artifactId>
<version>0.4</version>
<configuration>
<configFile>${project.basedir}/code-style.xml</configFile>
</configuration>
<executions>
<!-- <execution> -->
<!-- <goals> -->
<!-- <goal>format</goal> -->
<!-- </goals> -->
<!-- </execution> -->
</executions>
</plugin>
<!-- 单元测试与覆盖率 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version><!--$NO-MVN-MAN-VER$-->
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit47</artifactId>
<version>2.21.0</version>
</dependency>
</dependencies>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.21.0</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.7</version>
<configuration>
<formats>
<format>html</format>
<format>xml</format>
</formats>
</configuration>
</plugin>
<!--编译-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version><!--$NO-MVN-MAN-VER$-->
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- javadoc文档 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.1</version><!--$NO-MVN-MAN-VER$-->
<configuration>
<aggregate>true</aggregate>
</configuration>
</plugin>
<!-- spring boot -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version><!--$NO-MVN-MAN-VER$-->
<executions>
<execution>
<id>copy-lib-src-webapps</id>
<phase>package</phase>
<configuration>
<target>
<copy todir="${project.basedir}/docker">
<fileset dir="${project.basedir}/target">
<include name="*.jar" />
</fileset>
</copy>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.2.0</version>
<configuration>
<!-- imageName必须全为小写 -->
<imageName>springboot-unit-test:1.0</imageName>
<dockerDirectory>${project.basedir}/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
</project>

application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring.application.name=springadminserver
server.port=8080
#using the info endpoint
info.tags.environment=test
#配置eureka
eureka.client.serviceUrl.defaultZone=${EUREKA_SERVICEURL}
eureka.instance.prefer-ip-address=true
spring.cloud.inetutils.preferredNetworks=${PREFERRED_IP_PATTERN:.*}
eureka.instance.preferIpAddress=true
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.instance.leaseRenewalIntervalInSeconds=10
eureka.instance.health-check-url-path=/actuator/health
eureka.client.registryFetchIntervalSeconds=5
#公开的站点
management.endpoints.web.exposure.include=*
management.endpoints.health.show-details=ALWAYS
# 安全密码
spring.security.user.name=user
spring.security.user.password=123456

SpringBootAdminApplication

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
package com.springamdinserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;

import de.codecentric.boot.admin.server.config.AdminServerProperties;
import de.codecentric.boot.admin.server.config.EnableAdminServer;

@Configuration
@EnableAutoConfiguration
@EnableAdminServer
@EnableDiscoveryClient
public class SpringBootAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootAdminApplication.class, args);
}

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

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

@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
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();
// @formatter:on
}
}
}

客户端配置

1
2
3
4
management.endpoints.web.exposure.include=*
management.endpoints.health.show-details=ALWAYS
##日志路径
logging.path=/applog/logincenterWxBindingService

重启服务

控制台:
http://localhost:8080/
logo
eurka上服务
logo
每个服务 内存情况
logo
查看日志
logo
流量情况:
logo

参考:https://github.com/codecentric/spring-boot-admin

查询rabbitmq 队列信息

最近需要做mq 消息预警,消息一旦堆积某个数量就报警。

RabbitMQ自己就提供了HTTP API手册,比如我本地的API手册地址为:http://localhost:15672/api

http://localhost:15672/api/queues/%2F/soaSyncPwdQueue
可以看到队列相关的所有信息都有记录。

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
{
"consumer_details": [
{
"arguments": {

},
"channel_details": {
"connection_name": "10.143.172.86:35833 -> 10.210.93.176:5672",
"name": "10.143.172.86:35833 -> 10.210.93.176:5672 (1)",
"node": "rabbit@CNQLS03247",
"number": 1,
"peer_host": "10.143.172.86",
"peer_port": 35833,
"user": "logincenter"
},
"ack_required": true,
"consumer_tag": "amq.ctag-Gvgmd8yxscKvBkBiGp3J3g",
"exclusive": false,
"prefetch_count": 0,
"queue": {
"name": "soaSyncPwdQueue",
"vhost": "/"
}
}
],
"arguments": {
"x-dead-letter-exchange": "",
"x-dead-letter-routing-key": "DL_soaSyncPwdQueue"
},
"auto_delete": false,
"backing_queue_status": {
"avg_ack_egress_rate": 0.00021645676867156752,
"avg_ack_ingress_rate": 0.00021645676867156752,
"avg_egress_rate": 0.00021645676867156752,
"avg_ingress_rate": 0.00021645676867156752,
"delta": [
"delta",
"undefined",
0,
0,
"undefined"
],
"len": 0,
"mode": "default",
"next_seq_id": 2177,
"q1": 0,
"q2": 0,
"q3": 0,
"q4": 0,
"target_ram_count": "infinity"
},
"consumer_utilisation": null,
"consumers": 1,
"deliveries": [

],
"durable": true,
"effective_policy_definition": [

],
"exclusive": false,
"exclusive_consumer_tag": null,
"garbage_collection": {
"fullsweep_after": 65535,
"max_heap_size": 0,
"min_bin_vheap_size": 46422,
"min_heap_size": 233,
"minor_gcs": 294
},
"head_message_timestamp": null,
"idle_since": "2019-05-29 7:34:01",
"incoming": [

],
"memory": 18808,
"message_bytes": 0,
"message_bytes_paged_out": 0,
"message_bytes_persistent": 0,
"message_bytes_ram": 0,
"message_bytes_ready": 0,
"message_bytes_unacknowledged": 0,
"message_stats": {
"ack": 2007,
"ack_details": {
"rate": 0.0
},
"deliver": 2181,
"deliver_details": {
"rate": 0.0
},
"deliver_get": 2181,
"deliver_get_details": {
"rate": 0.0
},
"deliver_no_ack": 0,
"deliver_no_ack_details": {
"rate": 0.0
},
"get": 0,
"get_details": {
"rate": 0.0
},
"get_no_ack": 0,
"get_no_ack_details": {
"rate": 0.0
},
"publish": 2177,
"publish_details": {
"rate": 0.0
},
"redeliver": 4,
"redeliver_details": {
"rate": 0.0
}
},
"messages": 0,
"messages_details": {
"rate": 0.0
},
"messages_paged_out": 0,
"messages_persistent": 0,
"messages_ram": 0,
"messages_ready": 0, //消息未消费
"messages_ready_details": {
"rate": 0.0
},
"messages_ready_ram": 0,
"messages_unacknowledged": 0, //正在消费
"messages_unacknowledged_details": {
"rate": 0.0
},
"messages_unacknowledged_ram": 0,
"name": "soaSyncPwdQueue",
"node": "rabbit@CNQLS03247",
"operator_policy": null,
"policy": null,
"recoverable_slaves": null,
"reductions": 23652972,
"reductions_details": {
"rate": 0.0
},
"state": "running",
"vhost": "/"
}

注意:
虚拟主机名Virtual host在设置的时候不要带/,不然会访问不到

1
{"error":"Object Not Found","reason":"\"Not Found\"\n"}

之前就是被这个坑了好久,明明按照API写的格式来的,就是访问不到。
java代码:

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
jdk 1.8
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Base64;

public class Test {
public static void main(String[] args) throws Exception {
String host = "";
String port = "";
String virtualHost = "";
String queueName = "";
// 发送一个GET请求
HttpURLConnection httpConn = null;
BufferedReader in = null;

String urlString = "http://" + host + ":" + port + "/api/queues/" + virtualHost + "/" + queueName;
// urlString = "http://" + host + ":" + port + "/api/queues/";
URL url = new URL(urlString);
httpConn = (HttpURLConnection) url.openConnection();
// 设置用户名密码
String user="";
String password="";
String auth = user + ":" + password;

String encoding = new String(Base64.getEncoder().encode(auth.getBytes()));
httpConn.setDoOutput(true);
httpConn.setRequestProperty("Authorization", "Basic " + encoding);
// 建立实际的连接
httpConn.connect();
// 读取响应
if (httpConn.getResponseCode() == HttpURLConnection.HTTP_OK) {
StringBuilder content = new StringBuilder();
String tempStr = "";
in = new BufferedReader(new InputStreamReader(httpConn.getInputStream()));
while ((tempStr = in.readLine()) != null) {
content.append(tempStr);
}
in.close();
httpConn.disconnect();
System.out.println(content.toString());
} else {
System.out.println(httpConn.getResponseCode());
httpConn.disconnect();
}
}
}

resttemplate 问题

最近遇到一个问题:
mq消费 同步调用其他服务,突然不消费mq数据,且mq数据一直堆积

原因:
如果什么都不设置,RestTemplate默认使用的是SimpleClientHttpRequestFactory,其内部使用的是jdk的java.net.HttpURLConnection创建底层连接,默认是没有连接池的,connectTimeout和readTimeout都是 -1,即没有超时时间

解决方案:
http链接池配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "http-pool")
@Data
public class HttpPoolProperties {

private Integer maxTotal;
private Integer defaultMaxPerRoute;
private Integer connectTimeout;
private Integer connectionRequestTimeout;
private Integer socketTimeout;
private Integer validateAfterInactivity;

}

配置文件

1
2
3
4
5
6
http-pool.maxTotal=200
http-pool.defaultMaxPerRoute=100
http-pool.connectTimeout=5000
http-pool.connectionRequestTimeout=1000
http-pool.socketTimeout=600000
http-pool.validateAfterInactivity=2000

RestTemplateConfig

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
@Configuration
public class RestTemplateConfig {

@Autowired
private HttpPoolProperties httpPoolProperties;

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

@Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}

@Bean
public HttpClient httpClient() {
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setMaxTotal(httpPoolProperties.getMaxTotal());
connectionManager.setDefaultMaxPerRoute(httpPoolProperties.getDefaultMaxPerRoute());
connectionManager.setValidateAfterInactivity(httpPoolProperties.getValidateAfterInactivity());
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(httpPoolProperties.getSocketTimeout()) //服务器返回数据(response)的时间,超过抛出read timeout
.setConnectTimeout(httpPoolProperties.getConnectTimeout()) //连接上服务器(握手成功)的时间,超出抛出connect timeout
.setConnectionRequestTimeout(httpPoolProperties.getConnectionRequestTimeout())//从连接池中获取连接的超时时间,超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
.build();
return HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.build();
}
}

引用

1
2
@Autowired
private RestTemplate restTemplate;

底层代码构造sql忽略NUll条件

代码没有判断查询的值null,导致把全部数据load内存里,这样系统崩溃

代码:com.test.common.jpa.repository.JpaRepoUtil

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

private static StringBuilder generateQlForQueryByProperties(Map<String, Object> propertiesMap,
StringBuilder ql, Map<Integer, Object> params, FindByPropertiesType findType) {
int index = 0;

if (propertiesMap != null && !propertiesMap.isEmpty()) {
boolean doDeleteAnd = false, doDeletewWere = true;
ql.append(" where ");
for (Entry<String,Object> entry: propertiesMap.entrySet()) {
Object value = entry.getValue();
//忽略null 的值
if (value != null) {
if (doDeletewWere)
doDeletewWere = false;

if (value instanceof String && (findType == null || FindByPropertiesType.Fuzzy.equals(findType))) {
String valueStr = (String) entry.getValue();
ql.append("upper(model." + entry.getKey() + ") like :propertyValue" + index + " and ");
params.put(index++, "%" + valueStr.trim().toUpperCase() + "%");
} else if(QueryValue.NULL.equals(value)) {
ql.append("model." + entry.getKey() + " is null and ");
} else if(QueryValue.NotNull.equals(value)) {
ql.append("model." + entry.getKey() + " is not null and ");
} else {
ql.append("model." + entry.getKey() + "=:propertyValue" + index + " and ");
params.put(index++, entry.getValue());
}

doDeleteAnd = true;
}
}
if (doDeleteAnd)
ql = ql.delete(ql.toString().lastIndexOf(" and "), ql.toString().length());
if (doDeletewWere)
ql = ql.delete(ql.toString().lastIndexOf(" where "), ql.toString().length());
}
return ql;
}

upload successful

总结:需要深度了解底层代码。

在windows下atom上搭建PlantUML书写环境

PlantUML是一款使用纯文本绘制UML图的开源软件。它的优点是能够帮助作者把精力集中到内容的书写上而不是格式的调整。本文介绍了一种在windows环境下atom编辑器上搭建PlantUML书写环境的方法。

工具准备

  • atom
  • graphviz
    Graphviz是一款开源图形可视化软件。Atom的PlantUML插件使用graphviz将PlantUML语言绘制成相应的图形。

    插件安装

    github上已经有开发者为atom开发了PlantUML相关的插件。这里推荐以下两个插件
  • language-plantuml
  • plantuml-viewer
    其中language-plantuml提供了PlantUML语法高亮支持,plantuml-viewer负责把PlantUML语句转化为对应的UML图。
    1
    2
    3
    4
    有很多博客使用plantuml-preview。本人试用了这两款插件,发现都能很好的支持PlantUML。
    但是plantuml-viewer更有优势:plantuml-viewer能够实时显示PlantUML语言描述的图形,
    并且可以通过鼠标滚轮流畅的调节图形大小。此外,plantuml-preview需要显式的配置java和plantuml.jar,
    而plantuml-viewer不需要,因此我推荐plantuml-viewer

安装方法很简单:atom下 File -> Settings -> Install
logo

配置插件

plantuml-viewer按如下配置
logo

  • Charset配置成utf-8在生成图形的时候中文不会乱码
  • graphviz从官网下载后安装到自定义位置,在plant-viewer中指定dot.exe路径

验证

用PlantUML生成工厂模式UML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@startuml
skinparam classAttributeIconSize 0
class Product
class ConcreteProduct
class Creator {
+ {abstract} FactoryMethod()
}
class ConcreteCreator {
+ FactoryMethod()
}
Product <|-- ConcreteProduct
Creator <|-- ConcreteCreator
ConcreteProduct <.l. ConcreteCreator
note left of Product: 定义工厂方法所创建的对象的接口
note right of Creator: 声明工厂方法,该方法返回一个Product类型的对象
note left of ConcreteProduct: 具体产品,实现了Product的接口
note right of ConcreteCreator: 重定义工厂方法以返回一个ConcreteProduct实例
@enduml

效果图
logo

plantuml语法

Solr的主从模式Master-Slave

摘要:

如今,为了提高Solr的搜索速度,使其具有很好的容灾能力,往往会配置SolrCloud,但在Solr4之前,还有一种很流行的方式,Master-Slave模式,为什么要提及这种方式,因为我们公司目前用的就是这种方式。

引入Master-Slave

Solr在查询的时候,特别忌讳进行写操作,因为它是IO阻塞型的。现在的流行的Elasticsearch就对此有很好的改进。在引入Master-Slave以后,将读写分配到不同的服务器上,你可以使用master来做索引,然后使用slaves来做查询。
1.在多台服务器上分别搭建好可以独立运行的Solr,参见这里
2.指定其中的一台为Master,只需要在SolrConifg.xml中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<requestHandler name="/replication" class="solr.ReplicationHandler">
<lst name="master">
<str name="enable">${enable.master:true}</str>
<!--
Create a backup after 'optimize'. Other values can be 'commit', 'startup'.
It is possible to have multiple entries of this config string.
Note that this is just for backup, replication does not require this.
-->
<str name="backupAfter">optimize</str>
<!-- Replicate on 'commit'. 'startup' and 'optimize' are also the valid values for replicateAfter. -->
<str name="replicateAfter">commit</str>
<!-- If configuration files need to be replicated give the names here, separated by comma -->
<str name="confFiles">schema.xml,dict.txt,synonyms.txt</str>
<str name="commitReserveDuration">00:00:10</str>
</lst>
<int name="maxNumberOfBackups">2</int>
</requestHandler>

3.指定其他多有的服务为Slave,只需要分别SolrConifg.xml配置:

1
2
3
4
5
6
7
8
9
<requestHandler name="/replication" class="solr.ReplicationHandler" >
<lst name="slave">
<str name="enable">${enable.slave:true}</str>
<str name="masterUrl">http://192.168.1.102:8983/solr/jcg</str>
<str name="pollInterval">00:00:10</str>
<str name="httpConnTimeout">5000</str>
<str name="httpReadTimeout">10000</str>
</lst>
</requestHandler>

4.重启所有的master-slave服务即可

|