MyBatis中的#与$符号的区别

Mybatis中有很多可以学习的地方,#和$是两种常见的值替换方式,今天站在源码的角度去分析其解析过程。

关键源码

因为这段代码功能单一,对后续的流程影响不大,搞清楚这段代码的作用,基本上#{}${}的区别,在源代码上已经是清楚了。了解了这段代码之后,再进行后续的解析流程,就不会陷入这段代码的逻辑中,将整个视野投入到流程当中。

位置:GenericTokenParser.java –> parse()

1
2
3
4
5
6
7
8
9
10
private final String openToken;
private final String closeToken;
// 当匹配到#{}或${}后,对其中的文本进行的操作。
private final TokenHandler handler;

public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}

首先它有3个成员变量,可以理解成:当匹配到已openToken开头,closeToken结尾的字符串后,对其中的字符串执行handler.handleToken()方法。

这个handler就是一个典型的策略模式。它是一个只含有``函数的接口:

1
2
3
public interface TokenHandler {
String handleToken(String content);
}

也就是说#{}${}的处理逻辑都是封装到handleToken中。

寻找是否含有#{}${}的逻辑如下:

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
public class GenericTokenParser {
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}

用到GenericTokenParser的地方如下图所示:

用到的地方

仔细看一看上面的图片,会有一个疑问,那就是为什么#{}${}分别有2种不同的解析方法?这个可以在后面的分析中得出答案。这里直接给出我们平常所认为的那种处理方式的源代码,即#变成?,然后$直接变成相应的值,如下:

  • #{}的处理方式

    位置:SqlSourceBuidler.java –> ParameterMappingTokenHandler

    1
    2
    3
    4
    5
    @Override
    public String handleToken(String content) {
    parameterMappings.add(buildParameterMapping(content));
    return "?";
    }
  • ${}的处理方式

    位置:PropertyParser.java –> VariableTokenHandler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Override
    public String handleToken(String content) {
    if (variables != null) {
    String key = content;
    if (enableDefaultValue) {
    final int separatorIndex = content.indexOf(defaultValueSeparator);
    String defaultValue = null;
    if (separatorIndex >= 0) {
    key = content.substring(0, separatorIndex);
    defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
    }
    if (defaultValue != null) {
    return variables.getProperty(key, defaultValue);
    }
    }
    if (variables.containsKey(key)) {
    return variables.getProperty(key);
    }
    }
    return "${" + content + "}";
    }

从上面的代码来看,基本上的问题已经解决了。下面是MyBatis对XML中SQL语句的解析流程的分析。

示例代码

1
2
3
4
5
6
7
8
public static void main(String[] args) throws IOException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession session = sqlSessionFactory.openSession();

BizDriverMapper mapper = session.getMapper(BizDriverMapper.class);
List<BizDriver> list = mapper.getByNameOrNumber("hello");
System.out.println(list.size());
}

构建SqlSessionFactory阶段

SqlSessionFactoryBuilder.java –> build(InputStream, String, Properties)

1
2
3
4
5
6
7
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
}
...
}

XMLConfigBuilder.java –> parse()

1
2
3
4
5
6
7
8
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

XMLConfigBuilder.java –> parseConfiguration()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

XMLConfigBuilder.java –> mapperElement()

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
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}

XMLMapperBuilder.java –> parse()

1
2
3
4
5
6
7
8
9
10
11
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}

parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

XMLMapperBuilder.java –> configurationElement()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 处理sql的入口
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}

处理sql的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}

XMLStatementBuilder.java –> parseStatementNode()

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
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");

if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}

Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);

Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);

// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

其中createSqlSource()的对象langDriver来自XMLLanguageDriver

XMLLanguageDriver.java –> createSqlSource()

1
2
3
4
5
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}

关键步骤:XMLScriptBuilder.java –> parseScriptNode()

这里面的每一行代码都可以仔细去看、并理解它的意思

1
2
3
4
5
6
7
8
9
10
11
12
13
public SqlSource parseScriptNode() {
// 判定是否含有${},如果有,isDynamic-->true
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
if (isDynamic) {
// 有${}不做任何处理
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 没有${},把#{}都替换成?
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

通过上面的分析可知,会有两种情况,即是否为动态。一条语句开始的时候都被包装成MixedSqlNode,然后再通过判断是否为动态,将其封装成DynamicSqlSourceRawSqlSource。这两种的含义是:DynamicSqlSource${}不做任何处理,RawSqlSource没有${},把#{}都替换成?。是如何分配的呢?是通过GenericTokenParser来寻找$符号,然后在handler中,将isDynamic标记为true,代码如下:

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
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// isDynamic()中封装了对GenericTokenParser的调用
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
XMLScriptBuilder.NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}

isDynamic()中封装了对GenericTokenParser的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
private static class DynamicCheckerTokenParser implements TokenHandler {

private boolean isDynamic;

public DynamicCheckerTokenParser() {
// Prevent Synthetic Access
}

public boolean isDynamic() {
return isDynamic;
}
// 将本对象的变量置为true
@Override
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
}

在处理RawSqlSource时的初始化过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 所有的#{}都将被替换成?
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}

// SqlSourceBuilder.java
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

其中ParameterMappingTokenHandlerhandleToken()方法,也就是匹配成功后的回调,实现如下:

1
2
3
4
5
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}

在处理RawSqlSource时,没有这方面的处理,因此在这里就不贴其代码流程。反思:因为这是初始化阶段,可以理解成开机启动脚本,所以#{}的值是确定的,也就是说要变成,而${}的值在这个阶段就是未知的,因为还没有具体的SQL语句要生成,直接替换的数据还不确定,所以含有${}的语句,在初始化阶段是先不处理。

参考:

nc的简易使用

端口扫描

nc -zvn 192.168.126.135 22

  • z 参数告诉netcat使用0 IO,连接成功后立即关闭连接, 不进行数据交换.
  • v 参数指详细输出.
  • n 参数告诉netcat 不要使用DNS反向查询IP地址的域名.

聊天室

  • 服务器角色:nc -l 1081
  • 客户端角色:nc 192.168.126.135 1081

不管你在机器B上键入什么都会出现在机器A上。

演示

文件传输

服务器客户端传文件

  • 服务器角色:nc -l 1081 < test.txt
  • 客户端角色:nc -n 192.168.126.135 1081 > test.txt

客户端服务器传文件

  • 服务器角色:nc -l 1081 > test.txt
  • 客户端角色:nc -n 192.168.126.135 1081 < .bashrc

目录传输

服务器发送目录

  • 服务器角色:tar -cvf - cpp | nc -l 1081
  • 客户端角色:nc -n 192.168.126.135 1081 | tar -xvf -

服务器接收目录

  • 服务器角色:nc -l 1081 | tar -xvf -
  • 客户端角色:tar -cvf - test_dir | nc -n 192.168.126.135 1081

以上的列举是比较常用的。但是存在一个问题是传输速度可能不快。

Flask如何使用logging.FileHandler将日志保存到文件

需求

将日志尽可能往文件中输,自带的默认只输出到屏幕上。

代码

获取文件名

1
2
3
4
5
6
7
8
9
10
11
def get_custom_file_name():
def make_dir(make_dir_path):
path = make_dir_path.strip()
if not os.path.exists(path):
os.makedirs(path)
return path
log_dir = "ac_logs"
file_name = 'logger-' + time.strftime('%Y-%m-%d', time.localtime(time.time())) + '.log'
file_folder = os.path.abspath(os.path.dirname(__file__)) + os.sep + log_dir
make_dir(file_folder)
return file_folder + os.sep + file_name

配置logging

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dictConfig({
'version': 1,
'formatters': {'default': {
'format': '%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s',
}},
'handlers': {
'default': {
'class': 'logging.StreamHandler',
'stream': 'ext://flask.logging.wsgi_errors_stream',
'formatter': 'default'
},
'custom': {
'class' : 'logging.FileHandler',
'formatter': 'default',
'filename' : get_custom_file_name(),
'encoding' : 'utf-8'
},
},
'root': {
'level': 'INFO',
'handlers': ['custom']
}
})

代码分析

在官方文档中,有一个默认的handler,当我添加一个自定义的handler,名叫custom的时候,读取配置失败,程序中断,也就无法继续执行下去,提示说,少一个叫做filename的参数

loggin.FileHandler的构造函数中,有一个必填的参数,叫做filename。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FileHandler(StreamHandler):
"""
A handler class which writes formatted logging records to disk files.
"""
def __init__(self, filename, mode='a', encoding=None, delay=False):
"""
Open the specified file and use it as the stream for logging.
"""
# Issue #27493: add support for Path objects to be passed in
filename = os.fspath(filename)
#keep the absolute path, otherwise derived classes which use this
#may come a cropper when the current directory changes
self.baseFilename = os.path.abspath(filename)
self.mode = mode
self.encoding = encoding
self.delay = delay

如何才能传入参数filename?

①进入dictConfig()

1
2
3
def dictConfig(config):
"""Configure logging using a dictionary."""
dictConfigClass(config).configure()

②进入configure(),找到对handler的处理逻辑,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 前面的代码省略
# 获取handlers
handlers = config.get('handlers', EMPTY_DICT)
deferred = []
# 遍历其中的每一个handler
for name in sorted(handlers):
try:
# 具体处理每一个handler
handler = self.configure_handler(handlers[name])
handler.name = name
handlers[name] = handler
except Exception as e:
if 'target not configured yet' in str(e.__cause__):
deferred.append(name)
else:
raise ValueError('Unable to configure handler '
'%r' % name) from e
# 后面的代码省略

③进入具体处理handler的逻辑,self.configure_handler()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def configure_handler(self, config):
# 省略代码若干行 ...
if '()' in config:
c = config.pop('()')
if not callable(c):
c = self.resolve(c)
factory = c
else:
cname = config.pop('class')
klass = self.resolve(cname)
# 省略代码若干行 ...
# 此处的kclass就是配置的class名所对应的类
factory = klass
props = config.pop('.', None)
# 读取完类名,获取到该类、获取了formatter之后,接着读取conf里面的数据
# 在这里将剩下所定义的参数,弄成dict类型的数据
kwargs = {k: config[k] for k in config if valid_ident(k)}
try:
# 直接将dict类型的数据作为函数入参,实例化出一个FileHandler
result = factory(**kwargs)
except TypeError as te:
if "'stream'" not in str(te):
raise

其中的调试数据截图如下:
在这里插入图片描述
如果没有配置filename的信息,那么实例化类的时候就报错也是理所应当,因此还可以尝试性地配置encoding参数到其中,工作正常。

总结

虽然对这一块的整体了解还不够,但是对于能够参照官方文档,参考源代码实现,完成自己的想法,做到的那一刻还是有点成就感。

【参考】
官方文档:http://flask.pocoo.org/docs/dev/logging

MySQL查询卡死、无返回结果问题解决

现象

①在Navicat中,修改表结构,点击保存,然后发现Navicat卡住,无法正常退出,且MySQL无数据返回。
②在任务管理器中强制关闭了Navicat后,重复在Navicat中尝试几次,结果仍然一样。
③在MySQL CLI中进行select查询,同样卡住。

分析

起初认为是网络问题,但是想到可能是被堵塞住了。

解决

查询MySQL中的进程:show processlist

打开正在进行中的进程列表,发现有Waiting for table metadata lock,所以初步判定是因为某个操作被堵塞,然后后续操作无法执行,从而引起了这个问题。

从网上的资料看来,可能的原因是有未提交事物,阻塞DDL,继而阻塞所有同表的后续操作;结合自己对此数据库的操作,初步认为是在自己的小项目里面,可能存在上述情况。

查看未提交的事务:select trx_state, trx_started, trx_mysql_thread_id, trx_query from information_schema.innodb_trx\G

执行后,发现确实存在未提交的事务。

kill掉未提交的事务,返回到Navicat,发现 之前卡住的操作已完成

【参考】
https://blog.csdn.net/benben683280/article/details/78799010
https://blog.csdn.net/u013235478/article/details/68062939
https://www.cnblogs.com/digdeep/p/4892953.html

关于String与常量池的问题

面试必问题吧,但是好像还有一个比较容易漏的地方。总体的流程,搞清楚执行此代码后,他们的结果是什么,以及为什么就差不多算理解了。String a1 = new String("ab");System.out.println(a1 == a1.intern());//falseStr
阅读更多

解决使用MyBatis处理含有$的变量时报IllegalArgumentException Illegal group reference

现象

在一个GET请求中,输入参数包含$符号,然后后台报错。

复现

1
2
3
4
5
6
7
8
9
curl 'http://localhost:9999/xxxx/drivers?page=1&size=20&realname=$' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Accept-Language: zh,en;q=0.9,ja;q=0.8,zh-TW;q=0.7,fr;q=0.6,zh-CN;q=0.5' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36' \
-H 'Accept: application/json, text/plain, */*' \
-H 'userId: 453' \
-H 'Connection: keep-alive' \
-H 'token: 1e6966ec2fafdbbd53ef53124cc3e5ae' \
--compressed

报错截图

在这里插入图片描述

原因

这个现象与mybatis无关,因为mybatis对$符号的操作主要集中在GenericTokenParser.java中,而这些操作都是以\${出现,因此可以认为mybatis不处理单独的$符号。如下:
在这里插入图片描述
在interceptor中,有一段打印将要执行的sql语句相关信息,根据调试结果,锁定了这个方法出问题所在,如下:

1
2
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));

因此问题转化成了下面的问题:

1
2
3
4
5
6
// 有问题的语句
String sql = "er WHERE realname like ? AND deleted = ?";
sql = sql.replaceFirst("\\?", "\\%$%");
System.out.println(sql);
// 修改
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement("\\%$%"));

$在replaceFirst(String regex, String replacement)中的使用

网上关于这个方法的使用,普遍停留在regex,对replacement的解释少之又少,基本上没有。因为$符号在replacement中导致操作报错,因此,这个$肯定是有某种特殊的意义的。打开源代码,一直跟踪到抛出错误的地方。代码如下:

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
/**
* Processes replacement string to replace group references with
* groups.
*/
private StringBuilder appendExpandedReplacement(String replacement, StringBuilder result) {
int cursor = 0;
while (cursor < replacement.length()) {
char nextChar = replacement.charAt(cursor);
// 可以看出,转义符号使用起来也需要小心。如果在最后面,又会报错。
if (nextChar == '\\') {
cursor++;
if (cursor == replacement.length())
throw new IllegalArgumentException(
"character to be escaped is missing");
// 对转义符的操作比较简单,只是先跳过转义符,然后将下一个字符拷贝
nextChar = replacement.charAt(cursor);
result.append(nextChar);
cursor++;
// 如果是$,那么后面要么是数字,要么是{
} else if (nextChar == '$') {
// Skip past $
cursor++;
// Throw IAE if this "$" is the last character in replacement
if (cursor == replacement.length())
throw new IllegalArgumentException(
"Illegal group reference: group index is missing");
nextChar = replacement.charAt(cursor);
int refNum = -1;
if (nextChar == '{') {
cursor++;
StringBuilder gsb = new StringBuilder();
// 先读出{}中的内容
while (cursor < replacement.length()) {
nextChar = replacement.charAt(cursor);
if (ASCII.isLower(nextChar) ||
ASCII.isUpper(nextChar) ||
ASCII.isDigit(nextChar)) {
gsb.append(nextChar);
cursor++;
} else {
break;
}
}
if (gsb.length() == 0)
throw new IllegalArgumentException(
"named capturing group has 0 length name");
if (nextChar != '}')
throw new IllegalArgumentException(
"named capturing group is missing trailing '}'");
String gname = gsb.toString();
if (ASCII.isDigit(gname.charAt(0)))
throw new IllegalArgumentException(
"capturing group name {" + gname +
"} starts with digit character");
//{}中包含的内容,是分组的名字,分组存在,直接拿来分组的值,不存在则报错
if (!parentPattern.namedGroups().containsKey(gname))
throw new IllegalArgumentException(
"No group with name {" + gname + "}");
refNum = parentPattern.namedGroups().get(gname);
cursor++;
} else {
// The first number is always a group
refNum = nextChar - '0';
// 只接受0-9的数字,也就是分组的序号
if ((refNum < 0) || (refNum > 9))
throw new IllegalArgumentException(
"Illegal group reference");
cursor++;
// Capture the largest legal group string
boolean done = false;
while (!done) {
if (cursor >= replacement.length()) {
break;
}
int nextDigit = replacement.charAt(cursor) - '0';
if ((nextDigit < 0) || (nextDigit > 9)) { // not a number
break;
}
int newRefNum = (refNum * 10) + nextDigit;
if (groupCount() < newRefNum) {
done = true;
} else {
refNum = newRefNum;
cursor++;
}
}
}
// Append group
if (start(refNum) != -1 && end(refNum) != -1)
result.append(text, start(refNum), end(refNum));
} else {
result.append(nextChar);
cursor++;
}
}
return result;
}

可以看出,转义符号使用起来也需要小心。如果在最后面,又会报错,对转义符的操作比较简单,只是先跳过转义符,然后将下一个字符拷贝;如果是$,那么后面要么是数字,要么是{。对$符号的处理,如果后面是{,则先读出{}中的内容,也就是分组的名字,然后查询分组是否存在,存在则直接拿来分组的值,不存在则报错,如果后面是数字,就去拿第n个分组的值。

过程如上,但是网上基本上没有上述内容的示例,根据上面的简要分析,做出了下面简要的demo:

1
2
3
4
5
String Str = new String("Welcome to Tutoririalspoint.combb");
System.out.print("Return Value :" );
System.out.println(Str.replaceFirst("Tuto(.*)als(?<hi>.*)(bb)", "$1AMROOD}$2--$3--${hi}--xx"));
// 输出:
// Return Value :Welcome to ririAMROOD}point.com--bb--point.com--xx

给分组命名确实以前没尝试过,参考:https://stackoverflow.com/questions/415580/regex-named-groups-in-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
private String showSql(Configuration configuration, BoundSql boundSql) {
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));

} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
}
}
}
}
return sql;
}

参考:
https://github.com/abel533/Mapper/issues/30

MyBatis分页插件PageHelper封装以及遇到的bug

PageHelper链接:https://github.com/pagehelper/Mybatis-PageHelper
项目中使用到了一个注解,叫做PageAble,这是一个对PageHelper的封装注解。这个注解有一个非常显著的问题就是,不能在这个方法里面执行两次SQL查询(原因将在后续中慢慢分析)。使用方法如下:

1
2
3
4
@PageAble
public Object method(int page, int size) {
。。。
}

注解的内容比较简单,就是定义了两个参数,分别为这两个参数设置了默认的名字、以及默认值。

1
2
3
4
5
6
7
8
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PageAble {
String pageSizeName() default "size";
String pageNumName() default "page";
int pageSize() default 20;
int pageNum() default 1;
}

然后得到的返回值是一个叫做ResultPageView的类,是对分页情况的一个封装,其中的内容如下:

1
2
3
4
5
6
7
public class ResultPageView<T> {
private Long total = 0l;
private Integer current = 1;
private Integer pageCount = 0;
private List<T> list;
// 省略一些构造方法、getter/setter方法
}

所以,最重要的问题当然是@PageAble注解的方法是怎样执行的。显然这里是利用了Spring AOP,在这个方法的前后,加上了自定义的处理方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static final String PAGE_ABLE = "@annotation(com.xxxxxo0o0.baseservice.annotation.PageAble)";
@Around(PAGE_ABLE)
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
logger.info("execute method : " + proceedingJoinPoint.getSignature().getName());
try {
// 准备开始分页
prepare(proceedingJoinPoint);
// 执行被注解的方法
Object obj = proceedingJoinPoint.proceed();
// 装饰被注解方法返回的值
Object result = after(obj);
return result;
} catch (Throwable throwable) {
logger.error("aspect execute error : ", throwable);
throw throwable;
} finally {
// 先忽略这个finally里面的内容
//PageHelper.clearPage();
}
}

在被注解方法执行前的准备活动中,执行了什么操作?代码如下:

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
private void prepare(ProceedingJoinPoint point) throws Exception {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
PageAble pageAble = targetMethod.getAnnotation(PageAble.class);
// 获取参数名称
String numName = pageAble.pageNumName();
String sizeName = pageAble.pageSizeName();
// 先获取page和size的默认值
int pageNo = pageAble.pageNum();
int pageSize = pageAble.pageSize();
// 获取参数值列表
Object[] paramValues = point.getArgs();
// 获取参数名列表
String[] paramNames = methodSignature.getParameterNames();
int length = paramNames.length;
// 对参数列表进行遍历
for (int i = 0; i < length; i++) {
// 如果参数名 == 注解中写入的页数参数名
if (paramNames[i].equals(numName)) {
// 从参数值列表中取出值,赋值给页数
pageNo = (Integer) paramValues[i];
// 如果参数名 == 注解中写入的每页数量的参数名
} else if (paramNames[i].equals(sizeName)) {
// // 从参数值列表中取出值,赋值为每页尺寸
pageSize = (Integer) paramValues[i];
}
}
// 调用PageHelper的分页
PageHelper.startPage(pageNo, pageSize);
}

先忽略其中的细节,看看被注解方法后面执行的方法做了什么事情,代码如下:

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
private Object after(Object obj) {
assert obj instanceof List;
PageInfo<?> pageInfo = new PageInfo((List<?>) obj);
// 从某个地方获取的分页的信息。(其实是ThreadLocal,先忽略)
Page<Object> localPage = PageHelper.getLocalPage();
// 获取分页参数
long total = localPage.getTotal();
int pageNum = localPage.getPageNum();
int pages = localPage.getPages();
List<?> list = (List<?>) obj;
try {
List<Map> mapList = new ArrayList<>();
for (Object o : list) {
// 将一个对象按照原来的字段名转成map
HashMap<String, Object> map = MapUtil.convertObj2Map(o);
if (o instanceof BaseModel) {
BaseModel baseModel = (BaseModel) o;
map.put("id", baseModel.getId());
}
ReflectionUtils
.doWithFields(o.getClass(), new InnerFieldCallback(map, o), new InnerFieldFilter());
mapList.add(map);
}
list = mapList;
} catch (Exception e) {
logger.error("convert obj to map occurred error ", e);
}
pageInfo = new PageInfo((list));
ResultPageView<?> resultPageView;
resultPageView = new ResultPageView<>(total, pageNum, pages, pageInfo.getList());
// 清除分页信息
PageHelper.clearPage();
return resultPageView;
}

PageHelper.start()做了什么

一路往父类翻到start()的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
* @param reasonable 分页合理化,null时用默认配置
* @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}

setLocalPage()与之前的after()中的getLocalPage()是一对get/set方法,他们的目的是从当前线程中获取/设置分页信息。其实现如下(关于ThreadLocal的具体实现,可以去参考其他博客):
在这里插入图片描述
既然startPage()只是在线程中塞了一个关于分页的信息,那么真正读取这个分页信息的动作一定是在处理SQL语句的地方,也就是Interceptor。PageHelper的官方使用文档链接:
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md
其中也有一块,是对不安全分页的说明:

PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelperfinally 代码段中自动清除了 ThreadLocal 存储的对象。如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时),这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。但是如果你写出下面这样的代码,就是不安全的用法:

1
2
3
4
5
6
7
PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}

这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

因此打开项目中的MyPageInterceptor,它的功能就是充当Mybatis的拦截器,还有一部分自定义的功能,比如说输出sql执行时间、打印sql语句。这个类与PageHelper的拦截器关键的代码基本一致,可以说是copy吧,其中关键的一个地方是intercept()方法中,有一个进行判断,是否需要分页的语句。
在这里插入图片描述
在查询完毕后,finally方法回执行一次清除动作:
在这里插入图片描述
这个dialect是一个本地的类,继承自PageHelper这个类,覆盖了其中的afterAll()方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyPageHelper extends PageHelper {

@Override
public void afterAll() {
// 获取分页信息
Page<Object> localPage = getLocalPage();
// 调用父类方法,即清除分页信息
super.afterAll();
// 又将分页信息塞回线程中。
// 为什么要这样做?为了让在切面中,加入分页的详细信息。
setLocalPage(localPage);
}
}

这里的代码执行完成后,不论查询的结果是成功还是失败,分页信息都会存在当前线程中(如果直接调用父类的方法,不自定义这个方法,就能保证执行完一次查询,分页信息不会保存在当前线程中)。问题就出在这里。因为interceptor处理过后,当前线程中还存在分页的信息,并且这个分页的信息需要以来切面的处理方法来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
if(ms.getId().endsWith(MSUtils.COUNT)){
throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
}
Page page = pageParams.getPage(parameterObject, rowBounds);
if (page == null) {
return true;
} else {
//设置默认的 count 列
if(StringUtil.isEmpty(page.getCountColumn())){
page.setCountColumn(pageParams.getCountColumn());
}
autoDialect.initDelegateDialect(ms);
return false;
}
}

其中获取Page的代码是这样的,说到底还是从当前线程中去取:
在这里插入图片描述

bug描述与分析

执行一个分页查询,让查询故意报错,多执行几次,然后再进行一次普通查询,得到ClassCastException异常。
因此,问题已经出来了。
在这里插入图片描述

解决

因此加上finally后,无论是否报错,那么分页信息都将会被在线程中清除。问题就解决了,所以把涂掉的finally加上清除分页信息的处理,即可解决此问题。

后记

批量发送请求的脚本,用来引发bug用:
main.py

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
import requests
import json

host1 = 'localhost:9999'
host2 = 'xxxxxxxx'
host = host1
MAX = 100

def vehicle():
parse_response(send_get_req('http://'+host+'/nemt/driver/get'))
parse_response(send_get_req('http://'+host+'/nemt/vehicles?page=1&size=20'))
parse_response(send_get_req('http://'+host+'/nemt/vehicleType/all'))
pass


def app_version():
parse_response(send_get_req('http://'+host+'/nemt/driver-apps'))


def parse_response(resp):
s = json.loads(resp[0].content)
if s['code'] == 500:
# print()
print("x " + s['message'] + ' --> ' + resp[1])
else:
print("o")
pass


def send_get_req(url):
# print('url --> '+url)
return requests.get(url), url


def main():
i = 0
while i <= MAX:
app_version()
vehicle()
i = i + 1


if __name__ == '__main__':
main()

crack.sh

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash
#if [$1 -eq ""]; then
# max=1
#else
# max=$1
#fi
for i in $(seq 1 $1):
do
curl 'http://localhost:9999/nemt/orders?page=1&size=20' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: zh,en;q=0.9,ja;q=0.8,zh-TW;q=0.7,fr;q=0.6,zh-CN;q=0.5' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36' -H 'Accept: application/json, text/plain, */*' -H 'userId: 453' -H 'Connection: keep-alive' -H 'token: 48143d9154e7c42face53855826f5ffa' --compressed
echo ''
done

curl设置header和表单数据

老是分不清楚header和表单时的写法,每次都去翻别人的博客

从浏览器中获取某个请求的curl版本

在这里插入图片描述
下面是几种上述方法拷贝出来的curl命令参数,特意列出来,供参考一下。

设置header与表单

GET请求

1
2
3
4
5
6
7
8
9
10
curl 'http://xxxxxxxxx/vehicles?page=1&size=20&number=8' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Accept-Language: zh,en;q=0.9,ja;q=0.8,zh-TW;q=0.7,fr;q=0.6,zh-CN;q=0.5' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36' \
-H 'Accept: application/json, text/plain, */*' \
-H 'Referer: http://xxxxxxxxx/' \
-H 'userId: 454' \
-H 'Connection: keep-alive' \
-H 'token: af0d8773933eaeffc81d97924bd2a8fc' \
--compressed

POST请求,发送body信息
--data-binary DATA HTTP POST binary data (H)

1
2
3
4
5
6
7
8
9
10
11
12
13
curl 'http://xxxxxxxxx/nemt/nemt/order/cancel' 
-H 'Origin: http://xxxxxxxxx'
-H 'Accept-Encoding: gzip, deflate'
-H 'Accept-Language: zh,en;q=0.9,ja;q=0.8,zh-TW;q=0.7,fr;q=0.6,zh-CN;q=0.5'
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'
-H 'Content-Type: application/json'
-H 'Accept: application/json, text/plain, */*'
-H 'Referer: http://xxxxxxxxx/'
-H 'userId: 454'
-H 'Connection: keep-alive'
-H 'token: af0d8773933eaeffc81d97924bd2a8fc'
--data-binary '{"id":"10076","reasonId":0}'
--compressed

POST请求,发送表单
--data DATA HTTP POST data (H)

1
2
# 此例来自:http://www.ruanyifeng.com/blog/2011/09/curl.html
curl -X POST --data "data=xxx" example.com/form.cgi

文件上传

1
2
3
4
5
6
# 此例来自项目中某安卓端代码
do_upload(){
curl -X POST -F "file=@$1" $2
}
# 此例来自:http://www.ruanyifeng.com/blog/2011/09/curl.html
curl --form upload=@localfilename --form press=OK [URL]

OK,清理书签完毕!