0%

教训:nacos配置一定要验证可用性之后再去验证性能

feign+okhttp 与 feign+httpclient开启压缩的方式是不一样的!

服务端返回值开启压缩

服务端添加配置

1
2
3
4
5
6
7
8
9
10
server:
compression:
enabled: true
min-response-size: 100
mime-type:
- text/html
- text/xml
- application/xml
- application/octet-stream
- application/json

客户端接收服务端返回信息进行自动解压缩

okhttp自带自动解压缩,代码在okhttp3.internal.http.BridgeInterceptor

httpclient 未知

测试验证

1
2
3
4
5
6
7
8
http POST http://127.0.0.1:8080/ping
HTTP/1.1 200
Connection: close
Content-Encoding: gzip
Content-Type: application/json
Date: Fri, 03 Feb 2023 04:56:39 GMT
Transfer-Encoding: chunked
Vary: accept-encoding

客户端发送压缩请求

方案1: feign_okhttp_undertow

此方案可以利用undertow的自动解压缩能力,方便快捷,但是风险点是会影响现有配置及监控(我们有大量的tomcat配置及监控)。

服务端添加如下依赖配置

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
undertow:
io-threads: 64
worker-threads: 500
compression:
enabled: true
min-response-size: 100
mime-type:
- text/html
- text/xml
- application/xml
- application/octet-stream
- application/json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class WebServerConfig {

@Bean
public UndertowServletWebServerFactory undertowServletWebServerFactory() {
UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
factory.addDeploymentInfoCustomizers((deploymentInfo ->
deploymentInfo.addInitialHandlerChainWrapper(httpHandler ->
new RequestEncodingHandler(httpHandler).addEncoding("gzip", GzipStreamSourceConduit.WRAPPER)
)
));
return factory;
}
}

客户端添加如下配置

1
2
3
4
5
6
feign:
client:
okhttp:
enabled: true
httpclient:
enabled: false
1
2
3
4
5
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>${version.feign.okhttp}</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Slf4j
public class OkHttpConfig {
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.addInterceptor(new OkGzipCompressionInterceptor())
.connectTimeout(2, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(500, 5L, TimeUnit.MINUTES))
.build();
}
}
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
@Slf4j
public class OkGzipCompressionInterceptor implements Interceptor {
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request originalRequest = chain.request();
if (originalRequest.body() == null || !StringUtils.equalsIgnoreCase(originalRequest.header("Content-Encoding"), "gzip")) {
return chain.proceed(originalRequest);
}

Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), compress(originalRequest.body()))
.build();
return chain.proceed(compressedRequest);
}

private RequestBody compress(RequestBody body) {
return new RequestBody() {
@Nullable
@Override
public MediaType contentType() {
return body.contentType();
}

@Override
public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException {
BufferedSink gzipSink = Okio.buffer(new GzipSink(bufferedSink));
body.writeTo(gzipSink);
gzipSink.getBuffer().size();
gzipSink.close();
}
};
}
}

方案2: feign_okhttp_tomcat

服务端配置

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
public class GZIPUtils {

public static byte[] uncompress(byte[] src) throws IOException {
if (src == null || src.length == 0) {
return null;
}

ByteArrayInputStream in = new ByteArrayInputStream(src);
return uncompress(in);
}

public static byte[] uncompress(InputStream srcStream) throws IOException {
if (srcStream == null || srcStream.available() == 0) {
return null;
}

ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPInputStream ungzip = new GZIPInputStream(srcStream);
byte[] buffer = new byte[256];
int n;
while ((n = ungzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
return out.toByteArray();
}
}
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
/**
* 读取request 参数数据并包装为新的request
*/
@Slf4j
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {

/**
* requestBody数据
*/
private final byte[] body;

/**
* 所有参数的集合
*/
private Map<String, String[]> parameterMap;


public BodyReaderHttpServletRequestWrapper(HttpServletRequest request, boolean isCompress) throws IOException {
super(request);
if (isCompress) {
InputStream in = request.getInputStream();
if (in != null) {
body = readGzipBytes(in);
} else {
body = new byte[0];
}
} else {
BufferedReader reader = request.getReader();
if (reader != null) {
body = readBytes(reader);
} else {
body = new byte[0];
}
}
parameterMap = request.getParameterMap();
}

private byte[] readGzipBytes(InputStream srcStream) throws IOException {
return GZIPUtils.uncompress(srcStream);
}


@Override
public BufferedReader getReader() throws IOException {

ServletInputStream inputStream = getInputStream();

if (null == inputStream) {
return null;
}

return new BufferedReader(new InputStreamReader(inputStream));
}

@Override
public Enumeration<String> getParameterNames() {
Vector<String> vector = new Vector<>(parameterMap.keySet());
return vector.elements();
}

@Override
public ServletInputStream getInputStream() throws IOException {

if (body == null) {
return null;
}

final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {

@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener listener) {

}

@Override
public int read() throws IOException {
return bais.read();
}
};
}

/**
* 通过BufferedReader和字符编码集转换成byte数组
*
* @param br
* @return
* @throws IOException
*/
private byte[] readBytes(BufferedReader br) throws IOException {
String str;
StringBuilder retStr = new StringBuilder();
while ((str = br.readLine()) != null) {
retStr.append(str);
}
if (StringUtils.isNotBlank(retStr.toString())) {
return retStr.toString().getBytes(StandardCharsets.UTF_8);
}
return new byte[0];
}

public String inputStream2String(InputStream inputStream) {
if (Objects.isNull(inputStream)) {
return "";
}

StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.error("Fail to populate request body", e);
}
return sb.toString();
}
}
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
@Component
@WebFilter(urlPatterns = "/*")
@Slf4j
public class RequestContentFilterHaha implements Filter {

private final static String CONTENT_ENCODING = "Content-Encoding";
private final static String GZIP_TYPE = "gzip";

@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("Initializing request content filter ...");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
try {
String encodeType = httpServletRequest.getHeader(CONTENT_ENCODING);
BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest, GZIP_TYPE.equals(encodeType));
filterChain.doFilter(requestWrapper, servletResponse);
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.warn("read request request body error: {}, url: {}, method: {}", e.getMessage(), httpServletRequest.getRequestURL(), httpServletRequest.getMethod());
}
}
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}

@Override
public void destroy() {

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
compression:
enabled: true
min-response-size: 200
mimeTypes:
- text/html
- text/xml
- application/xml
- application/octet-stream
- application/json
feign:
okhttp:
enabled: true
httpclient:
enabled: false
max-connections: 1000
max-connections-per-route: 50
ribbon:
http:
client:
enabled: false
okhttp:
enabled: true

客户端配置

1
2
3
4
5
6
7
8
9
10
11
12
13
feign:
okhttp:
enabled: true
httpclient:
enabled: false
max-connections: 1000
max-connections-per-route: 50
ribbon:
http:
client:
enabled: false
okhttp:
enabled: true
1
2
3
4
5
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>${version.feign.okhttp}</version>
</dependency>

在做单元测试过程中,我们常常需要对某些字段进行注入Mock

1
2
3
4
5
void reflectSet(Test target, String fieldName, Object value) {
def field = Test.getDeclaredField(fieldName)
field.setAccessible(true)
field.set(target, value)
}

但是有一类数据我们是不方便注入的,例如:

1
2
3
4
5
6
@Slf4j
public class Test {
public void testLog() {
log.info("testLog");
}
}

1
2
3
4
5
6
public class Test {
private final static Logger log = LoggerFactory.get(Test.class);
public void testLog() {
log.info("testLog");
}
}

如果我们需要对log的行为进行测试,那么这里的注入应该怎么搞呢?

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
def "test"() {
given:
def log = Mock(Logger)
def test = new Test()
reflectSetFinalStatic(test, "log", log)
when:
service.testLog()
then:
1 * log.info(_) >> {
args->{
assert args[0] == "testLog"
}
}
noExceptionThrown()
}


void reflectSetFinalStatic(Test target, String fieldName, Object value) {
def field = Test.getDeclaredField(fieldName)
field.setAccessible(true)
// 修改访问权限
def modifiers = field.getClass().getDeclaredField("modifiers")
modifiers.setAccessible(true)
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL)
field.set(target, value)
// 恢复访问权限
modifiers.setInt(field, field.getModifiers() & Modifier.FINAL)

}

java操作mysql过程中,发现部分操作耗时是在事务过程中的,可以通过以下方法来进行定位。

1
2
3
4
5
6
-- 查看日志是否开启
show variables like 'general_log';
-- 开启日志
set global general_log=on;
--查看通用日志位置
show variables like '%general_log_file%'

通过监控日志详情,发现事务过程中有大量的select @@SESSION.'tx_read_only'指令伴随事务在执行。

通过配置mysql连接,加入参数useLocalSessionState=true得到解决。

详细代码栈:

1
ConnectionImpl.isReadOnly

问题

问题描述:你在使用分布式锁对代码加一个锁,会不会业务没有执行完,锁释放了?

1
setnx?set?

如果回答setnx就掉入一个坑了

1
setnx key value

没有过期时间?没有过期时间就意味着我们必须有可靠且安全的措施保障最终锁能被我们自己手动释放,如以下代码

1
2
3
4
5
6
7
8
9
10
if (setnx('key', 1)) {
try {
// do buisness
} catch(Exception e) {
// handle exception
} finally {
// release lock
del('key')
}
}

有了finally是不是就真的安全了?tomcat崩了,jvm死了,机器断电,遭雷劈了?

阅读全文 »

背景

通常我们在服务提供交互时,经常会被要求提供api文档,各种情况和格式要求都会有,如pdfshowdocword以及html等。其中做甲方项目的苦力面对最多的就是我们今天的目的格式wordswagger2有很多现成的可以支持的(可不可用需要自己测试),但是我为了偷懒文档生成,直接使用了最新的swagger3,接下来就是我不断折腾找工具,改工具的过程记录和最后的缝合怪。

折腾

经过搜索后找到一个不能用的swagger2word,而且只支持swagger2。看了几眼,改造成本过大,放弃。

然后找到官方维护的swagger-parser,这个倒是支持swagger3,但是这个有一个问题就是需要自己去写模版来套对象做输出(心累,收藏夹里吃灰吧)。

那么最后瞄准一个已经遗弃的项目swagger2markup,作者没有太多的精力去维护,但是本身功能完善,文档详尽,但是!它不支持swagger3。而且它支持的格式为ASCIIDOC,MARKDOWN。但是我有神器Typora啊(这篇文章也是用这个编辑器写的)。支持导出为pdfhtmlword。所以决定用它了。

本想通过引入新版本的swagger-parser,让其直接支持swagger3openapi spec 不,我不想,太累人了。

最后思路变为:

swagger3生成json->想办法将其转化为swagger2->使用swagger2markup生成markdown->Typora将markdown转化为word->完美Over

阅读全文 »

最小化Ioc容器使用样例

什么是容器?容器是一种为某种特定组件的运行提供必要支持的一个软件环境。例如,Tomcat就是一个Servlet容器,它可以为Servlet的运行提供运行环境。类似Docker这样的软件也是一个容器,它提供了必要的Linux环境以便运行一个特定的Linux进程。IoC容器,可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。我们平时接触最多的就是Spring Framework,此篇中还会展示另外一个IoC容器google guice的使用例子。

原始样例

先创建一个空的maven工程。

1
2
3
4
5
6
7
8
package org.chaos;

public class Main {
public static void main(String[] args) {
BusinessService businessService = new BusinessService();
System.out.println(businessService.doBusiness("chaos"));
}
}
1
2
3
4
5
6
7
8
9
package org.chaos;

public class BusinessService {

public String doBusiness(String name) {
PrintService printService = new PrintService();
return "I need do print with " + printService.print(name);
}
}
1
2
3
4
5
6
7
package org.chaos;

public class PrintService {
public String print(String name) {
return "English print with " + name;
}
}

这里BusinessService直接强依赖于PrintService,如果使用者(这里是Main)根据特定条件来打印内容,就会遇到问题。例如

1
2
3
4
5
if (currentLocation.equals("中国")) {
//希望调用doBusiness然后执行中文打印
} else {
//希望调用doBusiness然后执行英文打印
}

这里就需要用到控制反转,肯定的是这里也不能使用具体的实现类了,不然我们需要根据具体的实现类编写多个doBusiness, 这也是IoC容器的基本功能体现。(常规做法实际上是先将new改为工厂类,从直接依赖接耦为间接依赖)

阅读全文 »

上一篇完成数据库初始化之后,我们需要配置工程的一些基本配置元素。

先建立包结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── annotations
├── configurations
├── controllers
├── exceptions
├── interceptors
├── mappers
├── pojos
│   ├── bos
│   ├── dos
│   ├── dtos
│   ├── enums
│   ├── transfers //mapstruct转换器专用
│   └── vos
└── services

建立一套从servicemappervodo的访问代码,这里以sys_user表为例。

阅读全文 »

从今天起,开始打造一套属于自己的业务开发代码模版,本系列不会直接提供代码,只会展示部分关键性代码片段。

建立java模版

进入https://start.spring.io/

选择:

  • Spring Web
  • Lombok

  • Validation

  • Mybatis Framework

  • MySQL Driver

  • Spring Boot DevTools

  • Flyway Migration

1
autoboot ~/Downloads/template.zip

这里是我自己定制的一个自动解压开intellij脚本。

阅读全文 »

缘起

主要对go每日一库Rust每周一库的拾人牙慧,不过学习嘛,不寒碜。哈哈。

今天是第一天,我们从命令行参数解析库——Clap开始。

简介

相较于golangrust的解析命令参数等非语言核心能力并非是在核心库中直接提供的,而是由三方库提供。

三方库就会有一个选择问题,特别是对于不熟悉的新手。

我们可以通过lib.rs网站中Categories中选择Command-line interface来查看对应的库排名来初步选择。

如:https://lib.rs/keywords/argument

对于参数解析,使用量最大的就是Clap

仓库:https://github.com/clap-rs/clap

文档:https://docs.rs/clap/3.0.0-beta.5/clap/

视频教程:https://www.youtube.com/playlist?list=PLza5oFLQGTl2Z5T8g1pRkIynR3E0_pc7U

阅读全文 »