GeXiangDong

精通Java、SQL、Spring的拼写,擅长Linux、Windows的开关机

0%

今天用 brew upgrade 把所有brew 按照的都升级了,mysql被升级到了 8,之后mac 自带的 apache + php 7 就无法连接mysql了。(mysql命令行客户端没有问题)

php 提示错误:

PDOException::(“PDO::__construct(): The server requested authentication method unknown to the client [caching_sha2_password]”)

原因

mysql 改变了默认的密码插件,改成了 caching_sha2_password 模式,而 php 不支持造成的。

phpinfo() 中可以查看到 loaded plugins 没有 caching_sha2_password

有两个解决方案:

  • 给PHP增加 caching_sha2_password 模块
  • 更改mysql 8,改回 mysql_native_password 模式

两个方法任选一个即可。

更改 mysql8 的授权方法

注意以下步骤要按顺序:

  1. 使用mysql命令行登录,更改 root 用户的授权方式(也可再创建一个超级用户); SQL语句在文末

  2. 修改 my.cnf (mac brew 安装的在 /usr/local/etc/my.cnf),增加一行 default_authentication_plugin=mysql_native_password

  3. 重启 mysql (brew services restart mysql)

    如果在重启之前没有修改用户的密码插件,会提示密码错误,无法连接。

1
2
3
4
5
6
7
8
9
10
/** 创建用户 admin **/
CREATE USER 'admin'@'localhost' identified with mysql_native_password by '12345';

GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION;



/** 更改 root 用户, 12345是新密码 **/
alter user 'root'@'localhost' identified with mysql_native_password by '12345';

让 PHP 支持 caching_sha2_password

使用 PHP 7.1.16 之前的版本或者 PHP 7.2(PHP 7.2.4 之前的版本) 不支持 caching_sha2_password。

升级PHP到最新版就好了。

使用 brew upgrade postgresql 把 mac 上的 postgresql server 从10升级成11后,发现无法正常启动了

查看 postgresql 的日志文件 /usr/local/var/log/postgres.log,发现如下两行说明了启动失败的原因

2019-09-14 09:39:57.355 CST [1586] FATAL:  database files are incompatible with server
2019-09-14 09:39:57.355 CST [1586] DETAIL:  The data directory was initialized by PostgreSQL version 10, which is not compatible with this version 11.5.

用如下命令可以迁移老的数据文件

1
brew postgresql-upgrade-database

创建无老数据的新服务器

也可以删除老数据库文件,重新建个干净的新数据库。

先删除老数据库目录 /usr/local/var/postgres

然后运行

1
initdb -U 当前用户名 -A trust  -D /usr/local/var/postgres 

-U 后面跟的是当前用户的用户名,这样用psql 就不需要每次都加个 -U 参数了
-A 后面的trust,表示信任登录,不用输入密码了

之后

1
2
brew services stop postgresql
brew services start postgresql

就可以启动起来了

可以用 createdb tempdb 来创建一个名字为 tempdb 的数据库

1
psql tempdb

就能直接进入psql

spring boot中 websocet 默认和 stomp 结合在一起,使用起来比较简单,有些规则汇总整理一下。

websocket 的配置

写一个配置类实现 WebSocketMessageBrokerConfigurer 接口, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {

// 只有用enableSimpleBroker打开的地址前缀才可以在程序中使用,使用没设置enable的前缀时不会出错,但无法传递消息
// 服务端发送,客户端接收
config.enableSimpleBroker("/topic");

// @MessageMapping注解的设置的地址的会和这个前缀一起构成客户端需要声明的地址(stompClient.send()方法的第一个参数)
// 客户端发送,服务端接收
config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//客户端调用的URL;
registry.addEndpoint("/socket");
}


}

WebSocketMessageBrokerConfigurer 有8个方法,都是 default 方法,因此仅仅实现自己需要的即可。

WebSocketMessageBrokerConfigurer 的官方文档在 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurer.html

接收客户端发来的消息

客户端发送来的消息,可以通过 Controller 来接收,像 SpringMVC 一样,写个单独的 Controller 接收, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class SocketController {


/**
* MessageMapping注解中配置的接收地址和WebScoketConfig中setApplicationDestinationPrefixes()设置的地址前缀
* 一起构成了客户端向服务器端发送消息时使用地址
*/
@MessageMapping("/chat")
public void toAll(ChatMessage message, Principal principal) throws Exception {
// principal 是当前用户; 如果没有设置用户,则为null
// message 是发送过来的消息,可以是POJO,也可以用Map接收
}

}

给客户端发送消息

给客户发端发送消息有2种方式:

通过注解 @SendTo 和 @SendToUser

这两个注解使用有些局限,只能在上面 “接收客户端发来的消息” 用的 Controller 上,而且必须是被 @ MessageMapping 注解修饰的方法上,且仅当客户端触发(客户端给对应的地址发消息后,spring 框架调用这个方法,不是程序主动调用这个方法)时才起作用。

@SendTo 和 @SendToUser 注解有个参数,是发送给客户端的地址(客户端订阅消息的地址),发送的消息就是这个方法的返回值;

@SendTouser 是把消息发送给此方法触发时接收到的消息的发送者。也就是说:客户端给服务端发送一个消息,接受消息的是这个方法,这个方法的返回值作为回馈的消息又发回给了这个用户。(如果该用户没有订阅这个地址是不发的)

@SendTo 则是发送给所有用户(订阅了这个地址的所有用户,下同)

代码例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

@MessageMapping("/joinchat")
@ SendToUser("/topic/message")
public ChatMessage joinAndWelcome(ChatMessage message, Principal principal) throws Exception {
//由于增加了SendToUser注解,返回结果会被convertAndSend给特定用户,调用这个方法发消息的用户principal中的用户
return new ChatMessage("welcome, " + principal.getName() + ".");
}


@MessageMapping("/chat")
@SendTo("/topic/message")
public ClockMessage oneMessageToAll(ChatMessage message, Principal principal) throws Exception {
//发送给所有用户
return new ChatMessage("" + principal.getName() + ":" + message.getMessage() + " ");
}


使用组件 SimpMessagingTemplate

这个组件由 spring 创建,使用时只需要 Autowire 进来就好了。可以在程序任意地方使用,有发送给一个用户和所有用户的方法。

1
2
3
4
5
6
7
8
9
@Autowired
private SimpMessagingTemplate template;

public void sendMessage(){
// 发送给所有用户
this.template.convertAndSend("/topic/message", new ChatMessage("Hello, all!"));
// 发送给一个用户,用户ID 'xxx-yyy'
convertAndSendToUser("xxx-yyy", "/topic/message", new ChatMessage("Hello, xxx-yyy!"));
}

websocket 和 Filter

在 spring 中,我们可能使用/注册了一些 Filter (例如 spring 中的 OncePerRequestFilter), 普通的HTTP请求都先经过这些filter,然后交由具体的 controller 处理,filter的作用很多,例如最常见的权限验证。

每个 websocket 请求也会先经过这个filter,每个websocket请求仅仅是在最初连接的时候经过filter,以后每次发消息时,连接是已经建好的,自然也不会再过filter了。

因此如果有权限验证的 filter, websocket 可以和普通 HTTP 请求采用同样的权限验证方法,用同一个cookie或者同一个 Authentication Token 等。这仅仅帮助我们减少些代码,websocket 端还是需要写设置用户的,在下面讲。

设置websocket的用户

websocket中的用户是使用 Principal 类,和 Spring Security 有些差异。 为了获取当前用户并让后续的 controller 知道当前用户是谁,可以这么做

下面是扩充了的 WebSocketMessageBrokerConfigurer 配置,修改部分有注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 调用setHandshakeHandler方法设置一个自定义的HandShakeHandler
registry.addEndpoint("/socket/challenge").setHandshakeHandler(new MyHandshakeHandler());
}

/**
* 自定义的 HandshakeHandler, 覆盖了determineUser 方法,返回当前用户
*/
public class MyHandshakeHandler extends DefaultHandshakeHandler {

@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
// 这里查询当前用户,并返回一个 Principal 类作为当前用户,以后 controller 参数Principal 就是获得这里的返回值
// 获取当前用户可以通过 request 获得cookie/ token 然后查询获得
// 如果项目中也使用了 Springsecurity 也可用 SecurityContextHolder.getContext().getAuthentication() 获取当前登录用户
// 需要把当前用户设置成 Principal 类型,这个接口只有一个方法 getName() 这个name需要是每个用户唯一的,可以使用系统中用户的ID作为此处的name
}
}
}

需要注意的是 Principal 类型,这个类型只有一个方法 getName(), websocket 中用这个name来唯一识别一个用户,所以不要用用户名字作为这里的name,用用户的ID比较合适。

拒绝某些用户的连接

有些时候,我们希望在 websocket 服务端拒绝某些连接,例如携带不正确的 token 的用户的连接,使用上面的设置当前用户的方法是行不通的,因为 determineUser 方法即使返回 null 也会继续,只是当前用户为null; 可以在这个方法里抛一个异常,但这不优雅,非常不优雅。客户端收到的服务器端错误而连接失败,不是权限不足。

我们可以通过增加一个 HandshakeInterceptor 来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 通过addInterceptors(new MyHandshakeInterceptor()) 登记自定义的 HandshakeInterceptor
registry.addEndpoint("/socket/challenge").setHandshakeHandler(new MyHandshakeHandler()).addInterceptors(new MyHandshakeInterceptor());
}

public class MyHandshakeInterceptor implements HandshakeInterceptor {

@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
//返回false表示不接受此连接; 返回true表示接受此连接
return false;
}

@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {

}
}
}

设置心跳包

stomp 协议可以设置心跳包,多长时间给客户端发一个心跳包,多长时间接收客户端一个心跳包。心跳包的作用简单说就是告诉对方“我还活着”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 要设置心跳包,需要先设置一个 TaskScheduler
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1);
scheduler.setThreadNamePrefix("wss-heartbeat-thread-");
scheduler.initialize();

// 只有用enableSimpleBroker打开的地址前缀才可以在程序中使用,使用没设置enable的前缀时不会出错,但无法传递消息
// 服务端发送,客户端接收
// 通过setHeartbeatValue(new long[] {10000, 20000})设置心跳包频率,单位毫秒
config.enableSimpleBroker("/topic").setHeartbeatValue(new long[] {10000, 20000}).setTaskScheduler(scheduler);

// @MessageMapping注解的设置的地址的会和这个前缀一起构成客户端需要声明的地址(stompClient.send()方法的第一个参数)
// 客户端发送,服务端接收
config.setApplicationDestinationPrefixes("/app");
}
}

setHeartbeatvalue 的 long[] 数组参数长度是2,第一个是设置多长时间服务端向客户端发送一个心跳包,第二个值是多长时间收到客户端一个心跳包。默认都是0,也就是没有心跳包;但是如果设置了 setTaskScheduler,那么 hearbeatvalue 的默认值则是 10000,10000 (都是10秒钟)。

在程序中获取当前连接的用户

有些时候,我们希望知道当前有哪些用户连接,某个用户是否还在线等,可以通过组件 SimpUserRegistry 来获得,在需要的时候 Autowire 它即可。

例如:

1
2
3
4
5
6
7
8

@Autowired
private SimpUserRegistry userRegistry;

public void isUserOnline(String userId){
return userRegistry.getUsers().contains(userRegistry.getUser(userId));
}

userRegistry.getUsers()返回的是 Set<SimpUser> 类型,这个set包含所有的在线用户。 userRegistry.getUser(String id) 则是通过id(Principal类的name) 来查找当前用户,返回SimpUser类型。

ChannelInterceptor

可以给 websocket 注册多个不同的 ChannelInterceptor 来监视数据的往来,以及连接的建立、断开等,注册 interceptor 在 weboskcet 的配置中进行。

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
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
private static final Logger logger = LoggerFactory.getLogger(WebsocketConfig.class);

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// 注册一个自定义的拦截器
registration.interceptors(new MyChannelInterceptorAdapter());
}

public class MyChannelInterceptorAdapter implements ChannelInterceptor {

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

logger.trace("MyChannelInterceptorAdapter.preSend({}, {}) command is {}", message, channel,
(accessor.getCommand() == null ? "command is null" : accessor.getCommand().name()));


return message;
}
}
}

ApplicationListener 监听服务器websocket事件

监听服务端的各种websocket相关事件,可以通过实现 ApplicationListener 接口,然后把这个类标记为 Component 即可。

如果仅仅监听某种特定类型的事件,可以给 ApplicationListener 设定范型,标记自己需要监听的事件类型

1
2
3
4
5
6
7
8
9
@Component
public class SubscribeEventListener implements ApplicationListener<SessionSubscribeEvent> {
private static final Logger logger = LoggerFactory.getLogger(SubscribeEventListener.class);

@Override
public void onApplicationEvent(SessionSubscribeEvent event) {
logger.trace("SessionSubscribeEvent {}", event.getSource());
}
}

可以监听的事件很多,部分整理如下:

父类 子类
ApplicationContextEvent ContextClosedEvent
ContextRefreshedEvent
ContextStartedEvent
ContextStoppedEvent
AbstractSubProtocolEvent SessionConnectedEvent
SessionConnectEvent
SessionDisconnectEvent
SessionSubscribeEvent
SessionUnsubscribeEvent

同源问题

Spring websocket 默认加入了同源限制,如果客户端和服务端不是同源(协议、主机名、端口全部相同)会被禁止访问。当客户端是 APP、小程序、或不同源的HTML时,这就有问题了,可以通过 StompEndpointRegistrysetAllowedOrigins方法增加允许的源,例如 setAllowedOrigins("http://myhost.com:8081/,https://myhost.com:8082")等,也可以使用通配符 * 来表示所有。

1
2
3
4
5

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket/challenge").setHandshakeHandler(new MyHandshakeHandler()).setAllowedOrigins("*");
}

如果被同源禁止掉,会返回给客户端403

在小程序中的 wxss 中使用 @font-face 例如 @font-face {font-family: myfont; src: url('https://myhost.com/fonts/myfont.ttf');} 会在模拟器中报错:

VM2696:1 Failed to load font https://myhost.com/fonts/myfont.ttf
net::ERR_CACHE_MISS 

虽然在模拟器中能正常显示字体,但在真机中是不行的。

解决办法有2个:

小程序提供的API

使用小程序提供的 wx.loadFontFace, 先在js文件里加载字体,

1
2
3
4
5
wx.loadFontFace({
family: 'myfont',
source: 'url("https://myhost.com/fonts/myfont.ttf")',
success: console.log
})

wxss 文件里不用写@font-face,直接用 font-family: myfont 即可

文档在 https://developers.weixin.qq.com/miniprogram/dev/api/ui/font/wx.loadFontFace.html

使用base64字体 (CSS的标准方式)

也可把字体转成 WOFF/WOFF2/SVG 等格式, 并对二进制 base64 编码,嵌入到 css/wxss 文件中,不需要从网络上访问字体文件。

转化字体可以用 https://transfonter.org/ 这个网站提供的转化工具还可以只转换自己需要的几个文字,这样减少字体文件大小。

CSS文件会类似下面这个样子

1
2
3
4
5
6
7
@font-face {
font-family: 'myfont';
src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAA ... 这里会很长 ... EADQABAAA=) format('woff');
font-weight: bold;
font-style: normal;
}

小程序提供了 websocket 功能,STOMP 是在websocket之上的一个通讯协议, 使用起来比较简单容易。特别是后段的 spring boot 等支持STOMP over Websocket很简单。

可以通过在小程序段按照以下步骤加入 STOMP,

下载stomp.js 并加入到小程序中

可以从https://stomp-js.github.io/stomp-websocket/codo/extra/docs-src/Usage.md.html#地址下载最新的stomp.js, 这是官方地址,也有stomp.js的文档可供参考。

下载到stomp.js后,放入小程序的 /utils/ 目录下(也可以是其他目录)。

修改app.js,并包装websocket/stomp

javascript大部分方法都是异步,每个方法调用后,都需要在成功的回调函数里进行下一个,例如连接成功的回调函数里,才能发送/订阅stomp消息,否则就出错了,每个页面都这样写就比较麻烦了,因此我推荐在 app.js 里写两个公共函数,分别做发送和订阅功能,函数内处理连接的问题。代码如下:

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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
const BASEURL = 'http://localhost:8070/'
const SOCKETURL = 'ws://localhost:8070/socket'


App({
_websocket:{
client: null, // stomp client
subscriptions: [], //订阅都在此保存一份, websocket断掉后重连需要重新订阅
messagesQueue: [], // STOMP 消息队列 stomp连接建立前,调用发送会暂存于此
subscriptionQueue: [], // STOMP 订阅队列 stomp连接建立前,调用订阅会暂存于此
clientAviable: false // client是否可用,不可用包含连接建立前和建立后连接由于网络原因断开在尝试重连成功前
},

/**
* 发送STOMP消息的函数,供外部使用, 调用方式
* getApp().sendStomp('/app/something', message, header)
* message 是Object格式,此方法会转成JSON string
* header 可选,通常不需要
*/
sendStomp: function(dest, msg, header){
if (this._websocket.clientAviable){
// 连接已经建立起来了,直接发送
var res = this._websocket.client.send(dest, header, JSON.stringify(msg))
}else{
// 连接尚未建立,加入到消息队列,待连接成功建立后,自动发送
this._websocket.messagesQueue.push({
destination: dest,
header: header,
body: JSON.stringify(msg)
})
}
},

/**
* 订阅STOMP消息,供外部使用, 调用方式:
* getApp().subscribeStomp('/topic/news', function(){....收到消息后的回调函数...})
*/
subscribeStomp: function (dest, callback, header) {
// 先保存一份订阅;以便连接断掉重连时重新订阅
this._websocket.subscriptions.push({
destination: dest,
header: header,
callback: callback
})
if (this._websocket.clientAviable) {
// 连接已经建立,直接订阅
this._websocket.client.subscribe(dest, callback, header)
} else {
// 连接尚未建立,加入到订阅队列,待连接成功建立后,一起订阅
this._websocket.subscriptionQueue.push({
destination: dest,
header: header,
callback: callback
})
}
},

_sendSocketMessage: function(msg) {
console.log('send msg:', msg);
wx.sendSocketMessage({
data: msg
})
},

/**
* 连接weboscket,在onLunch里会调用,如果不是整个小程序都需要连接,也可在某个页面内调用
* 此方法会首先判断当前是否已经获取到了token,如果没token,会先获取token后连接
*/
connectWebsocket: function(param){
if (this.globalData.token) {
this._connectWebsocketWithToken(param)
} else {
//login
let that = this
this.login({ tokenGot: function () { that._connectWebsocketWithToken(param) } })
}
},

_closeSocket: function(pram){
wx.closeSocket({
success: function(res){},
fail: function(res){}
})
},

_connectSocket: function(){
let that = this
wx.connectSocket({
url: SOCKETURL,
header: {
"Authorization": "Bearer " + that.globalData.token
},
success: function(res){

},
fail:function(res){
}
})
},

_processQueue: function(client){
for (var i = 0; i < this._websocket.subscriptionQueue.length; i++) {
var subs = this._websocket.subscriptionQueue[i]
client.subscribe(subs.destination, subs.callback, subs.header)
}
this._websocket.subscriptionQueue = []
for (var i = 0; i < this._websocket.messagesQueue.length; i++) {
var msg = this._websocket.messagesQueue[i]
client.send(msg.destination, msg.header, msg.body)
}
this._websocket.messagesQueue = []
},

// 获取token后,连接websocket
_connectWebsocketWithToken: function(param){
let that = this
var ws = {
send: this._sendSocketMessage,
close: this._closeSocket,
onopen: null,
onmessage: null
}

this._connectSocket()

wx.onSocketOpen(function (res) {
console.log('websocket连接成功')

if (that._websocket.client) {
// 断掉自动重连的情形
// 首先重新订阅【订阅在重连后会丢失】
that._websocket.client.subscriptions = [] //清空以前的订阅
for (var i = 0; i < that._websocket.subscriptions.length; i++) {
var subs = that._websocket.subscriptions[i]
var result = that._websocket.client.subscribe(subs.destination, subs.callback, subs.header)
console.log('重新订阅', subs, result)
}
that._processQueue(that._websocket.client)
that._websocket.clientAviable = true
}

ws.onopen && ws.onopen()
})

wx.onSocketMessage(function (res) {
console.log('onSocketMessage事件:', res)
ws.onmessage && ws.onmessage(res)
})

//STOMP.js的断掉重连不起作用了,在这里设置断掉重连
wx.onSocketClose(function(res){
console.log('onSocketClose 事件,websocket断掉了:', res)
that._websocket.clientAviable = false
if(that._websocket.client != null){
//自动重连
setTimeout(that._connectSocket, that._websocket.client.reconnect_delay)
}
})

var Stomp = require('./utils/stomp.js').Stomp;

var client = Stomp.over(ws);

var headers = {}
var connectCallback = function (frame) {
console.log('STOMP client connect successed', frame)

that._processQueue(client)

that._websocket.client = client
that._websocket.clientAviable = true

if (param && param.success){
//调用 回调函数
param.success()
}
}

var errorCallback = function (e) {
console.log('STOMP connect error callback ', e)
}
client.debug = function(str){
console.log(str)
}
client.heartbeat.outgoing = 20000 // client will send heartbeats every 20000ms
client.heartbeat.incoming = 20000 // 0 meant disable
client.reconnect_delay = 3000 // stomp.js的断掉重连在这里不工作,这个设置给_connectSocket用
client.connect(headers, connectCallback, errorCallback)

},

/**
* onLaunch 方法会在小程序启动时调用
**/
onLaunch: function () {
let that = this
this._login({
tokenGot: function(){
console.log("login successed.", that.globalData.token)
//连接webosocket
that.connectWebsocket()
// 获取用户信息
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
wx.getUserInfo({
success: res => {
// 可以将 res 发送给后台解码出 unionId
that.globalData.userInfo = res.userInfo
}
})
}
}
})

}
})
},

_login: function (param){
let that = this
wx.login({
success(res) {
if (res.code) {
//发起网络请求
wx.request({
url: BASEURL + '/auth/login',
method: 'POST',
data: {
code: res.code
},
success: function (res) {
console.log("after post login data to server", res.data)
that.globalData.token = res.data.token
that.globalData.user = res.data.user
if (param && param.tokenGot) {
console.log("after set token")
param.tokenGot(res.data.token)
}
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
},

/**
* 重新封装的 request, 请求普通的url,增加token
*/
request: function(param) {
param.url = BASEURL + param.url
if (this.globalData.token) {
this._requestWithToken(param)
} else {
//login
let that = this
this._login({ tokenGot: function () { that._requestWithToken(param) } })
}
},

_requestWithToken: function (param) {
var header = param.header
if (header) {

} else {
header = {}
}
header['Authorization'] = 'Bearer ' + this.globalData.token
param['header'] = header
let successCallback = param.success
var resultHandler = function (res) {
//console.log(param, res)
if (res.statusCode >= 200 && res.statusCode < 300) {
if (successCallback) {
successCallback(res)
}
} else {
console.log("server error", res)
var error = {}
error.code = res.statusCode
if (res.statusCode >= 400 && res.statusCode < 500) {
error.message = "没找到资源,可能已删除"
} else if (res.statusCode >= 500 && res.statusCode < 600) {
error.message = "服务器出错了,请稍后再试"
} else {
error.message = "未知错误"
}
if (param.invalidResponse) {
param.invalidResponse(error, res)
}
}
}
param['success'] = resultHandler
console.log("request " + param.url + " with token " + this.globalData.token)
wx.request(param)
},

globalData: {
token: null
}
})

其他

取消订阅

这个例子没做取消订阅;取消订阅需要传递 header 的 id 参数,用这个 id 来取消订阅;如果没传递此参数,用 stompClient.subuscribe 的返回值内的 id 取消订阅。

setInterval clearInterval

网络上有些文章会在调用stomp时,设置两个方法,代码如下:

1
2
Stomp.setInterval = function () { ... }
Stomp.clearInterval = function () { ... }

这是因为他们使用的是旧版本的Stomp.js, 从官方网站下载的 stomp.js 不再需要这些代码。

后端

如果用spring boot的websocket,需要注意默认spring 禁止了非同源的访问,需要禁止掉,否则小程序端会抱403错误,无法连接。

客户端连接 spring websocket 返回403错误

心跳包需要后端也支持,可以 websocket 的配置中打开,下面例子的 setHeartbeatValue(new long[] {20000, 20000}) 是设置心跳包的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void configureMessageBroker(MessageBrokerRegistry config) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1);
scheduler.setThreadNamePrefix("wss-heartbeat-thread-");
scheduler.initialize();

// 只有用enableSimpleBroker打开的地址前缀才可以在程序中使用,使用没设置enable的前缀时不会出错,但无法传递消息
// 服务端发送,客户端接收
config.enableSimpleBroker("/topic").setHeartbeatValue(new long[] {20000, 20000})
.setTaskScheduler(scheduler);

// @MessageMapping注解的设置的地址的会和这个前缀一起构成客户端需要声明的地址(stompClient.send()方法的第一个参数)
// 客户端发送,服务端接收
config.setApplicationDestinationPrefixes("/app");
}

小程序的 WXML 语言中无 LI OL UL 等标签,只有 VIEW TEXT 等,要实现HTML中的 LI 功能,可以通过CSS来实现, 这个办法在 HTML 中也可行。

代码如下:

1
2
3
4
5
6
7
<view class="rulelist">
<view class="ruleitem">对战需要2个人参与</view>
<view class="ruleitem">同一题目两人抢答,先提交正确答案者得分</view>
<view class="ruleitem">正确答案得3分,错误答案一次扣1分</view>
<view class="ruleitem">20秒内两个选手均未提供正确答案,自动下一题</view>
<view class="ruleitem">得分先超过30分者获胜</view>
</view>
1
2
3
4
.rulelist{counter-reset: ord; padding-left:20px; }
.ruleitem{padding:2px 0; font-size:14px; line-height: 18px;}
.ruleitem:before{content: counter(ord) "."; counter-increment: ord; margin-left: -20px; display:inline-block; width:20px;}

counter-rest 这个属性还可修改序号的起始值、增长步长等。

今天用小程序连接 spring websocket服务器,小程序端一直报403错误。

VM1513 asdebug.js:1 WebSocket connection to 'ws://localhost:8080/socket/' failed: Error during WebSocket handshake: Unexpected response code: 403

检查后端spring websecurity授权,没有问题,后来发现是被websocket默认给禁止了非同源访问。

修改 websocket 的配置类,增加了 .setAllowedOrigins("*"),修改后如下:

1
2
3
4
5
6
7
8
9
10
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {

//其他部分略去

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//客户端调用的URL;
registry.addEndpoint("/socket").setAllowedOrigins("*");
}

改完后重启,小程序websocket就可以连接了。

现象

java9 启动 spring 项目时报错:

org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:157) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543) ~[spring-context-5.1.7.RELEASE.jar:5.1.7.RELEASE]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at cn.devmgr.springcloud.register.eureka.Application.main(Application.java:14) [classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:558) [spring-boot-maven-plugin-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at java.base/java.lang.Thread.run(Thread.java:844) [na:na]
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:125) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:86) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:427) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:180) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:181) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:154) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    ... 14 common frames omitted
Caused by: java.lang.IllegalStateException: StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[] failed to start
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.rethrowDeferredStartupExceptions(TomcatWebServer.java:171) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:109) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
    ... 19 common frames omitted

[WARNING] 
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:558)
    at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:157)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)
    at cn.devmgr.springcloud.register.eureka.Application.main(Application.java:14)
    ... 6 more
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:125)
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:86)
    at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:427)
    at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:180)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:181)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:154)
    ... 14 more
Caused by: java.lang.IllegalStateException: StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[] failed to start
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.rethrowDeferredStartupExceptions(TomcatWebServer.java:171)
    at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:109)
    ... 19 more

原因和解决办法

这是java9 默认没有加载 JAXB-API 造成的,java9开始分模块,默认加载的模块不包含 jaxb-api。 有几个办法可以解决:

  • 命令后增加参数 --add-modules java.xml.bind 引入此模块

  • 换java8

  • 手工增加 jaxb的依赖

    <dependency>
         <groupId>javax.xml.bind</groupId>
         <artifactId>jaxb-api</artifactId>
         <version>2.3.1</version>
     </dependency>
     <dependency>
         <groupId>com.sun.xml.bind</groupId>
         <artifactId>jaxb-impl</artifactId>
         <version>2.3.1</version>
     </dependency>
     <dependency>
         <groupId>org.glassfish.jaxb</groupId>
         <artifactId>jaxb-runtime</artifactId>
         <version>2.3.1</version>
     </dependency>
     <dependency>
         <groupId>javax.activation</groupId>
         <artifactId>activation</artifactId>
         <version>1.1.1</version>
     </dependency>

@Value和@ConfigurationProperties两个注解都可以从配置文件获取值并设置到属性中,用法上有区别。

@Value适用一些比较简单的情形,用得比较普遍;而@ConfigurationProperties则可适用一些@Value无法处理更复杂的情形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
myapp:
simple:
name: ABC
count: 15
countries: CN,US,UK
complex:
list:
- id: k1
value: hello
- id: k2
value: nihao
- id: k3
value: konichiha
nested:
name: John
address:
province: jiangsu
city: nanjing

例如上面的配置myapp.simple下的属性都可以用@value获取到,甚至myapp.simple.countries也可以被设置到String[]类型的属性上,但是下面的myapp.complex.list设置的配置,@Value就无法把它转成一个List<Map<String, Object>>类型的属性了,而@ConfigurationProperties可以。甚至myapp.complex.nested还可以被@ConfigurationProperties付值到带有两个属性(province, city)的子类中(参照下面的@ NestedConfigurationProperty注解)

@Value

可处理的类型

只可处理String、数值等简单类型,和简单的数组类型,不能处理map、List等复杂类型

位置

直接写在需要从配置文件读取值的成员变量上即可。
例如:

1
2
3
4
5
@Service
public class XxxxService{
@Value("${xxx.yyy}")
private String xxxYyy;
}

参数

只有一个字符型参数value,用于标记配置文件中的key

参数名称 类型 默认值 说明
value String 从配置文件中哪一项读取值,支持Spring EL

@ConfigurationProperties

可处理的类型

除了基本类型外,List、Map、 数组, 自定义POJO …

用法

@ConfigurationProperties注解需要写到类上,为该类所有有setter方法的属性从配置文件中读取并付值。@ConfigurationProperties 只能在类上使用,不能用在属性上。

如果类里面的某个属性是另外一个POJO,则在该属性上增加注解 @NestedConfigurationProperty , 这样就可嵌套解析。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ConfigurationProperties
public class xxxYyy{
private String name;
@NestedConfigurationProperty Address address;

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

public void setAddress(Address address){
this.address = address;
}
}

public class Address{
private String province;
private String city;

//---略过setter方法
}

参数

参数名称 类型 默认值 说明
ignoreInvalidFields boolean false java中被绑定的属性类型与配置文件中设置的值类型不同时是否忽略
ignoreUnknownFields boolean true Flag to indicate that when binding to this object invalid fields should be ignored
prefix String The name prefix of the properties that are valid to bind to this object.
value String 同prefix,简写,支持Spring EL

前置条件

1、需要增加依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

2、需要先开始使用配置属性的功能(不开启不出错,但不会读取配置并设置到类上)

1
@EnableConfigurationProperties

@EnableConfigurationProperties需要写在Spring Boot的启动类或配置类上。

从其他配置文件中读取

如果信息配置在了自定义的配置文件中(不是applicaiton.yml),则可以使用@PropertySource注解来告诉spring从哪个文件读取配置信息,例如:

1
@PropertySource("classpath:configprops.properties")

@PropertySource 注解默认只能解析 properties 文件,如果是yml文件,需要自定义一个解析器

如果需要从其他yml文件读取配置,可以利用 @PropertySource 的 factory 属性,先写一个解析yml 文件的类,这很简单,因为spring中已经包含了解析yml格式的解析器,我们只需要包装一下。

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
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;

import java.io.IOException;
import java.util.Properties;

public class YamlPropertySourceFactory implements PropertySourceFactory {
private static final Logger logger = LoggerFactory.getLogger(YamlPropertySourceFactory.class);

@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject();
logger.trace(
"read from {}, exists:{}",
encodedResource.getResource().getFile().getAbsoluteFile(),
encodedResource.getResource().exists());
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
}
}

之后在使用 @PropertySource 注解时增加 factory 属性

1
@PropertySource(factory = YamlPropertySourceFactory.class, value = "classpath:yamlfile.yml")

背景

有时客户端给服务器传输的JSON比较大,几兆甚至几十兆,网络传输占用时间较多,而json都是文本格式,压缩率较高。因此考虑传输压缩后的格式。

HTTP协议在相应部分支持Content-Encoding:gzip,压缩response body,而没有压缩request body的设计,这也很合理,因为在客户端发起请求时并不知道服务器是否支持压缩。因此没法通过http自身解决,只能增加些程序。

压缩和解压都消耗CPU时间,需要根据实际情况决定是否使用压缩方案,一般内网(微服务)调用启用压缩后有可能得不偿失。在网速较慢的情况下才会有意义。

方案

考虑到通用性,仿效response的header Content-Encoding:gzip方式。

客户端把压缩过的json作为post-body传输,然后增加一个request header: Content-Encoding: gzip来告诉服务器端是压缩的格式。

服务端增加一个Filter,对request头进行检查,如果有Content-Encoding则解压缩后继续。这样不影响现有程序。

服务端还有另外一个解决方案,在NGINX反向代理处增加一个插件(需要去找实现这种功能的插件,官方未提供)把解压缩后的request body传给应用服务器(tomcat)。

参照 http://www.pataliebre.net/howto-make-nginx-decompress-a-gzipped-request.html#.XBToGi276i5

实现

服务器端(Spring)

增加2个类:

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
package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Service
public class ContentEncodingFilter extends OncePerRequestFilter {
Logger logger = LoggerFactory.getLogger(ContentEncodingFilter.class);

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {

String conentEncoding = request.getHeader("Content-Encoding");
if(conentEncoding != null && ("gzip".equalsIgnoreCase(conentEncoding) || "deflate".equalsIgnoreCase(conentEncoding))){
logger.trace("Content-Encoding: {}", conentEncoding);
chain.doFilter(new GZIPRequestWrapper(request), response);
return;
}

chain.doFilter(request, response);
}
}

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
package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.zip.DeflaterInputStream;
import java.util.zip.GZIPInputStream;

public class GZIPRequestWrapper extends HttpServletRequestWrapper {
private final static Logger logger = LoggerFactory.getLogger(GZIPRequestWrapper.class);

protected HttpServletRequest request;

public GZIPRequestWrapper(HttpServletRequest request){
super(request);
this.request = request;
}

@Override
public ServletInputStream getInputStream() throws IOException {
ServletInputStream sis = request.getInputStream();
InputStream is = null;
String conentEncoding = request.getHeader("Content-Encoding");
if("gzip".equalsIgnoreCase(conentEncoding)){
is = new GZIPInputStream(sis);
}else if("deflate".equalsIgnoreCase(conentEncoding)){
is = new DeflaterInputStream(sis);
}else{
throw new UnsupportedEncodingException(conentEncoding + " is not supported.");
}
final InputStream compressInputStream = is;
return new ServletInputStream(){
ReadListener readListener;

@Override
public int read() throws IOException {
int b = compressInputStream.read();
if(b == -1 && readListener != null) {
readListener.onAllDataRead();
}
return b;
}

@Override
public boolean isFinished(){
try {
return compressInputStream.available() == 0;
} catch (IOException e) {
logger.error("error", e);
if(readListener != null) {
readListener.onError(e);
}
return false;
}
}

@Override
public boolean isReady() {
try {
return compressInputStream.available() > 0;
} catch (IOException e) {
logger.error("error", e);
if(readListener != null) {
readListener.onError(e);
}
return false;
}
}

@Override
public void setReadListener(final ReadListener readListener) {
this.readListener = readListener;
sis.setReadListener(new ReadListener() {
@Override
public void onDataAvailable() throws IOException {
logger.trace("onDataAvailable");
if(readListener != null){
readListener.onDataAvailable();
}
}

@Override
public void onAllDataRead() throws IOException {
logger.trace("onAllDataRead");
}

@Override
public void onError(Throwable throwable) {
logger.error("onError", throwable);
if(readListener != null){
readListener.onError(throwable);
}
}
});
}
};
}
}

客户端

CURL的测试命令

1
2
3
echo '{"type": "json", "length": 2222}' | gzip > body.gz
curl -v -i http://127.0.0.1:8080/ss -H 'Content-Encoding: gzip' -H 'Content-Type:application/json' --data-binary @body.gz

RestTemplate

使用RestTemplate可以通过配置interceptor来统一增加压缩。实现代码如下:

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
package cn.devmgr.springcloud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;

@Configuration
public class RestClientConfig {
private final static Logger logger = LoggerFactory.getLogger(RestClientConfig.class);

/**
* request body超过1M时,自动开启压缩模式;(需要服务端支持)
* 压缩和解压会增加程序处理时间,一般内网调用不压缩速度会更快;压缩只是在网络环境较差的情况下有使用意义。
* @return
*/
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();

List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new RestTemplateCompressInterceptor());

restTemplate.setInterceptors(interceptors);
return restTemplate;
}


class RestTemplateCompressInterceptor implements ClientHttpRequestInterceptor {

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
logger.trace("RestTemplateCompressInterceptor:intercept() body-length: {}", body == null ? 0 : body.length);
byte[] newBody = null;
if(body == null || body.length < 1024 * 1024){
// 小于1M不开启压缩模式
logger.trace("{}不需要压缩", body == null ? 0 : body.length);
newBody = body;
}else{
logger.trace("{}开启压缩模式", body.length);
request.getHeaders().add("Content-Encoding", "gzip");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzip;
try {
gzip = new GZIPOutputStream(baos);
gzip.write(body);
gzip.close();
} catch (IOException e) {
logger.error("压缩request body出错.", e);
throw(e);
}
newBody = baos.toByteArray();
logger.trace("压缩后大小 {}", newBody.length);
}
ClientHttpResponse response = execution.execute(request, newBody);
return response;
}
}
}