二、MyBatis常用类
2.1、XPathParser
XPath可以很简单的使用路径表达式在XML文档中选取节点元素。该工具的核心作用就是解析xml文件,包括我们的配置文件和mapper文件。这项技术也是爬虫的核心技术之一,下边我们了解一下xpath的基础语法,但这不是我们的重点,一切以了解为主。如下图:在浏览器中我们可以很轻松的获取一个标签的xpath。
2.1.1、基础语法
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
比如下面这个例子。
路径表达式 | 结果 |
---|---|
properties | 选取 properties元素的所有节点。 |
/properties | 选取根元素 properties。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
properties/property | 选取属于 properties的子元素的所有 property元素。 |
//property | 选取所有 property 元素,而不管它们在文档中的位置。 |
properties//property | 选择属于 properties元素的后代的所有 property元素,而不管它们位于 properties之下的什么位置。 |
//@lang | 选取名为 lang 的所有属性。 |
2.1.2、谓语(Predicates)
谓语是用来查找某个【特定的节点或者包含某个指定的值】的节点。通常,谓语被嵌在方括号中。在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果。
路径表达式 | 结果 |
---|---|
/properties/property[1] | 选取属于 properties子元素的第一个 property元素。 |
/properties/property[last()] | 选取属于 properties子元素的最后一个 property元素。 |
/properties/property[last()-1] | 选取属于 properties子元素的倒数第二个 property元素。 |
/properties/property[position()10.00] | 选取 properties 元素的所有 property元素,且其中的 value元素的值须大于 10.00。 |
/properties/property[value>10.00]/title | 选取 properties元素中的 property元素的所有 value元素,且其中的 price 元素的值须大于 10.00。 |
2.1.3、选取未知节点
XPath 通配符可用来选取未知的 XML 元素。
通配符 | 描述 |
---|---|
* | 匹配任何元素节点。 |
@* | 匹配任何属性节点。 |
node() | 匹配任何类型的节点。 |
比如下面这样。
路径表达式 | 结果 |
---|---|
/bookstore/* | 选取 bookstore 元素的所有子元素。 |
//* | 选取文档中的所有元素。 |
//title[@*] | 选取所有带有属性的 title 元素。 |
2.1.4、选取若干路径
通过在路径表达式中使用“|”运算符,您可以选取若干个路径。在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果。
路径表达式 | 结果 | |
---|---|---|
//book/title | //book/price | 选取 book 元素的所有 title 和 price 元素。 |
//title | //price | 选取文档中的所有 title 和 price 元素。 |
/bookstore/book/title | //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
2.1.5、测试用例
我们使用mybatis提供的XPathParser对mybatis.xml进行解析。
@Test
public void testXpathParser() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
XPathParser xPathParser = new XPathParser(inputStream, true, null, new XMLMapperEntityResolver());
XNode xNode = xPathParser.evalNode("/configuration/environments/environment/dataSource");
System.out.println(xNode);
}
2.2、OGNL表达式
在mybatis中的动态sql中存在很多表达式,如if标签中常见的(username != null && username != '')或者 #{id},为了解析这类标签,mybatis使用了OGNL技术,OGNL是 Object-Graph Navigation Language 的缩写,对象-图形导航语言,语法为:#{ }
。
2.2.1、OGNL三要素
2.2.1.1、表达式(Expression)
表达式是整个OGNL的核心,所有的OGNL操作都是针对表达式的解析结果进行处理的。表达式规定了此次OGNL操作到底要干什么。因此,表达式其实是一个带有语法含义的字符串,这个字符串将规定操作的类型和操作的内容。
OGNL支持大量的表达式语法,不仅支持“链式”描述对象访问路径,还支持在表达式中进行简单的计算,甚至还能够支持复杂的Lambda表达式等。
2.2.1.2、Root对象(Root Object)
OGNL的Root对象可以理解为OGNL的操作对象。当OGNL表达式规定了“干什么”以后,我们还需要指定对谁干。OGNL的Root对象实际上是一个Java对象,是所有OGNL操作的实际载体。这就意味着,如果我们有一个OGNL的表达式,那么我们实际上需要针对Root对象去进行OGNL表达式的计算并返回结果。
2.2.1.3、上下文环境(Context)
有了表达式和Root对象,我们已经可以使用OGNL的基本功能。例如,根据表达式针对OGNL中的Root对象进行“取值”或者“写值”操作。
不过,事实上,在OGNL的内部,所有的操作都会在一个特定的数据环境中运行,这个数据环境就是OGNL的上下文环境(Context)。说得再明白一些,就是这个上下文环境(Context)将规定OGNL的操作在哪里干。
OGNL的上下文环境是一个Map结构,称之为OgnlContext。之前我们所提到的Root对象(Root Object),事实上也会被添加到上下文环境中去,并且将被作为一个特殊的变量进行处理。
2.2.2、OGNL的基本操作
使用OGNL需要如下的依赖,但事实上我们并不需要显示引入该依赖,因为mybatis已经通过依赖传递的方式将其引入。
ognl
ognl
3.3.3
2.2.2.1、对Root对象(Root Object)的访问
针对OGNL的Root对象的对象树的访问是通过使用“点号”将对象的引用串联起来实现的。通过这种方式,OGNL实际上将一个树形的对象结构转化成了一个链式结构的字符串结构来表达语义。
@Test
public void testOgnlContext1() throws OgnlException {
// 定义一个参数
Account account = new Account();
account.setUsername("XiaoLin");
// 解析表达式,这种表达式在标签中的test中使用经常使用
Object value = Ognl.getValue("username == null && username == ''", account);
System.out.println("value = " + value);
}
2.2.2.2、对上下文环境(Context)的访问
由于OGNL的上下文是一个Map结构,在OGNL进行计算时可以事先在上下文环境中设置一些参数,并让OGNL将这些参数带入进行计算。有时候也需要对这些上下文环境中的参数进行访问,访问这些参数时,需要通过#符号加上链式表达式来进行,从而表示与访问Root对象(Root Object)的区别。
@Test
public void testOgnlContext2() throws OgnlException {
// 定义一个map上下文
Account account = new Account();
account.setUsername("tom");
Map map = new HashMap(4);
map.put("account",account);
Object username = Ognl.getValue("#account.username",map,new Object() );
System.out.println(username);
}
2.2.2.3、对静态变量的访问
在OGNL中,对于静态变量或者静态方法的访问,需要通过@[class]@[field|method]的表达式语法来进行访问。
// 访问@com.ydlclass.mybatissource.util.DBUtilBindThread@threadLocal类中名为ENABLE的属性值
@Test
public void testOgnlContext3() throws OgnlException {
Object enable = Ognl.getValue("@com.ydlclass.mybatissource.util.DBUtilBindThread@threadLocal",new Object() );
System.out.println(enable);
}
2.2.2.4、方法调用
在OGNL中调用方法,可以直接通过类似Java的方法调用方式进行,也就是通过点号加方法名称完成方法调用,甚至可以传递参数。这里我们就不做测试了。
@Test
public void testOgnlContext4() throws OgnlException {
// 定义一个map上下文
Account account = new Account();
account.setUsername("Maloney");
Map map = new HashMap(4);
map.put("account",account);
Object username = Ognl.getValue("#account.username.substring(1,5)",map,new Object() );
System.out.println(username);
}
2.3、别名注册器
mybatis提供了TypeAliasRegistry作为别名注册器,同时默认注入了大量的基础类型的别名,他是配置类的一个成员变量。
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
他的实现如下。
public class TypeAliasRegistry {
private final Map> resolverUtil = new ResolverUtil();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set>> typeSet = resolverUtil.getClasses();
for (Class type : typeSet) {
// 不是匿名类 | 不是接口 | 不是内部类
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
我们还可以进行单个别名的注册。
public void registerAlias(Class type) {
//
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
public void registerAlias(String alias, Class value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
typeAliases.put(key, value);
}
2.4、类型转化器
2.4.1、TypeHandler
TypeHandler负责对查询结果进b行类型转换,我们当然可以使用ResultSet的原生方法对结果进行类型转化,抽象模型从来的面向对象最难也是最灵活的内容,我们当然可以使用switch-case语法通过判断类型选择调用方法的版本如下。
public Object getResult(JDBCType jdbcType,ResultSet rs,String columnName) throws SQLException {
Object object = null;
switch(jdbcType){
case INTEGER -> object = rs.getInt(columnName);
case BIGINT -> object = rs.getLong(columnName);
case VARCHAR -> object = rs.getString(columnName);
//.... 省略其他众多的情况
}
return object;
}
很明显,以上这种代码的编写风格是一种面向过程的编码风格,对今后的扩展极为不利,以后每次新增类型都需要修改代码。
所以mybatis抽象出一个TypeHandler作为顶层接口,对转化工作做了模型抽离。
public interface TypeHandler {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
同时提供了大量的实现策略,(这样的实现本质就是策略设计模式),核心策略在构造的时候注册,新增的通过配置文件添加,极大的降低了系统的耦合性。
我们来写一个测试用例。
@Test
public void testTypeHandler(){
Connection connection = DBUtilBindThread.getConnection();
String sql = "select id,username,money from account";
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
TypeHandler typeHandler = new IntegerTypeHandler();
// 下边的代码本质就是resultSet.getInt("money")
Integer money = typeHandler.getResult(resultSet, "money");
System.out.println(money);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
同样我们看看IntegerTypeHandler的底层实现,这是策略模式的典型案例。
@Override
public Integer getNullableResult(ResultSet rs, String columnName)
throws SQLException {
int result = rs.getInt(columnName);
return result == 0 && rs.wasNull() ? null : result;
}
2.4.2、TypeHandlerRegistry
mybatis当然有提供一个专门的注册器TypeHandlerRegistry用来注册TypeHandler。TypeHandlerRegistry也是配置类的一个成员变量。
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
TypeHandlerRegistry的作用有两个:
我们来瞄一眼TypeHandlerRegistry的源码,他有这些成员变量。
我们会发现有一个老朋友——JdbcType。
我们点进去看看,里面有什么?我们可以发现mysql里面有的类型这里面都有。
我们根据一个jdbctype可以选定唯一一个jdbcTypeHandlerMap。事实上,我们一般情况会根据BaseTypeHandler的泛型来确定该处理器的javaType,从而确定使用的rs的getxxx和setxxx方法。JdbcType本质上没有什么具体的作用,更多的是作为一种标识,用来确定使用一个唯一的handler,在定义TypeHandler时需要指定泛型,也就意味着javaType一定会被指定,那么我们选取TypeHandler时就会有以下两种情况:
对于java的String类型,就有九个与之匹配的Typehandler。
我们来看一下BaseTypeHandler的实现。
public class ClobTypeHandler extends BaseTypeHandler {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
StringReader reader = new StringReader(parameter);
ps.setCharacterStream(i, reader, parameter.length());
}
@Override
public String getNullableResult(ResultSet rs, String columnName)
throws SQLException {
Clob clob = rs.getClob(columnName);
return toString(clob);
}
}
我们其实可以去瞄一眼TypeHandlerRegistry的构造方法。他一上来就给了我们兜底的对象。
他有一个默认实现ObjectTypeHandler。
点进去之后我们可以发现全部都是getObject。这就是兜底解决方案。
看完了构造器以后,我们可以开始往下看看register方法。
我们来逐行解析一下这段代码。
private void register(Type javaType, TypeHandler handler) {
if (javaType != null) {
Map, Reflector> reflectorMap = new ConcurrentHashMap();
public DefaultReflectorFactory() {
}
@Override
public Reflector findForClass(Class type) {
if (classCacheEnabled) {
// synchronized (type) removed see issue #461
return MapUtil.computeIfAbsent(reflectorMap, type, Reflector::new);
} else {
return new Reflector(type);
}
}
}
ReflectorFactory可以进行自定义。
2.5.5、Reflector
Reflector是一个反射工具,里边缓存了一个类的类型,setter方法,getter方法,默认构造器。
public class Reflector {
private final Class type;
private final String[] readablePropertyNames;
private final String[] writablePropertyNames;
private final Map setMethods = new HashMap();
private final Map getMethods = new HashMap();
private final Map> getTypes = new HashMap();
private Constructor defaultConstructor;
private Map caseInsensitivePropertyMap = new HashMap();
// 构建过程
public Reflector(Class clazz) {
type = clazz;
addDefaultConstructor(clazz);
Method[] classMethods = getClassMethods(clazz);
addGetMethods(classMethods);
addSetMethods(classMethods);
addFields(clazz);
readablePropertyNames = getMethods.keySet().toArray(new String[0]);
writablePropertyNames = setMethods.keySet().toArray(new String[0]);
// 处理大小写不敏感
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
private void addDefaultConstructor(Class clazz) {
Constructor[] constructors = clazz.getDeclaredConstructors();
Arrays.stream(constructors).filter(constructor -> constructor.getParameterTypes().length == 0)
.findAny().ifPresent(constructor -> this.defaultConstructor = constructor);
}
private void addGetMethods(Method[] methods) {
Map conflictingGetters = new HashMap();
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
.forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
resolveGetterConflicts(conflictingGetters);
}
private void addSetMethods(Method[] methods) {
Map conflictingSetters = new HashMap();
Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 1 && PropertyNamer.isSetter(m.getName()))
.forEach(m -> addMethodConflict(conflictingSetters, PropertyNamer.methodToProperty(m.getName()), m));
resolveSetterConflicts(conflictingSetters);
}
public Class getType() {
return type;
}
// ... 省略了大量的反射代码
}
2.5.6、setValue
我们一般使用metaObject的setValue("dept.id", 1)方法来赋值,我们来看一看这个MetaObject类的方法。
// setter,赋值操作
public void setValue(String name, Object value) { // name为需要赋值的属性,value为需要赋值的值
// 构造一个属性分析解析器
PropertyTokenizer prop = new PropertyTokenizer(name);
// 如果是非简单的属性,如:dept.id
if (prop.hasNext()) { // 看到hasNext,想到迭代器设计模式
// 如果dept没有值,需要先实例化dept
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
if (value == null) {
// 如果value为null,不要实例化子属性
return;
} else {
// 使用objectFactory构建一个employee的实例,
// objectFactory的默认实现就是使用反射创建一个实例
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
// metaValue -> dept , prop.getChildren() -> id
metaValue.setValue(prop.getChildren(), value);
} else {
// 普通属性的赋值,如name
objectWrapper.set(prop, value);
}
}
2.5.7、getValue
看完了setValue以后,我们肯定还需要再来看看getValue方法。我们可以发现很明显这是一个递归调用。
// 此处传入dept
public Object getValue(String name) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
return null;
} else {
// 此处使用了递归调用
return metaValue.getValue(prop.getChildren());
}
} else {
// 从实例中直接获取
return objectWrapper.get(prop);
}
}
在往上一级的调用是。
public MetaObject metaObjectForProperty(String name) {
Object value = getValue(name);
return MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory);
}
我们可以看看这个forObject方法,因为我们传进来的是一个字符串,也就是dept他,他没有实例化对象,所以此时会返回SystemMetaObject.NULL_META_OBJECT。
public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
// 如果获取的值为空
if (object == null) {
return SystemMetaObject.NULL_META_OBJECT;
} else {
return new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
}
}
2.5.8、属性分析器
属性分析器是PropertyTokenizer。我们来看看他的构造方法,这个属性分析器使用了迭代器设计模式。
public class PropertyTokenizer implements Iterator {
private String name;
private final String indexedName;
private String index;
private final String children;
public PropertyTokenizer(String fullname) {
// 用.去进行切割传进来的属性名
int delim = fullname.indexOf('.');
// 说明这个属性中有.,如dept.id,说明是复杂属性
if (delim > -1) {
// 截取.之前的内容作为name,此处是dept
name = fullname.substring(0, delim);
// .之后的作为children,此处是id
children = fullname.substring(delim + 1);
} else {
// 如果没有.,说明是简单属性,那么name就是fullname
name = fullname;
children = null;
}
indexedName = name;
// employees[1]
delim = name.indexOf('[');
if (delim > -1) {
// 1
index = name.substring(delim + 1, name.length() - 1);
// employee
name = name.substring(0, delim);
}
}
// 继续分析子属性
@Override
public PropertyTokenizer next() {
return new PropertyTokenizer(children);
}
// ...省略其他的setter和getter
}
2.5.9、构建成员变量实例
如果我们传进来一个dept字符串的话,那么并未实例化,但我们试图为其复制,所有必须通过反射进行实例化。
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
objectWrapper是一个接口,我们以他的实现BeanWapper的实现为例。
@Override
public MetaObject instantiatePropertyValue(String name, PropertyTokenizer prop, ObjectFactory objectFactory) {
MetaObject metaValue;
// 通过属性的名字获得属性的类型,com.xxx.Dept setDept(Dept dept) Dept.class
Class type = getSetterType(prop.getName());
try {
// 根据类型创建一个实例
Object newObject = objectFactory.create(type);
// 构建一个MetaObject
metaValue = MetaObject.forObject(newObject, metaObject.getObjectFactory(), metaObject.getObjectWrapperFactory(), metaObject.getReflectorFactory());
// 赋值
set(prop, newObject);
} catch (Exception e) {
throw new ReflectionException("Cannot set value of property '" + name + "' because '" + name + "' is null and cannot be instantiated on instance of " + type.getName() + ". Cause:" + e.toString(), e);
}
return metaValue;
}
我们进去getSetterType这个方法。
public Class getSetterType(String name) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObject.metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
return metaClass.getSetterType(name);
} else {
return metaValue.getSetterType(prop.getChildren());
}
} else {
return metaClass.getSetterType(name);
}
}
2.6、ErrorContext
在mybatis中为了更好的定位异常或错误,特封装了ErrorContext类,用于描述错误信息,他比传统异常机制要好很多,传统的异常打印出来都是一片一片的,内容特别多,但是ErrorContext输出的内容可以自由定制,它使用了ThreadLocal,可以非常好的将问题定位到某个线程,同时保证了线程安全。
我们来看一下ErrorContext打印出来的异常。随便把sql语句写错即可。他的错误提示更加精准,而且他也保留了原来的错误消息的提示。
我们来看一下源码。
public class ErrorContext {
private static final String LINE_SEPARATOR = System.lineSeparator();
private static final ThreadLocal LOCAL = ThreadLocal.withInitial(ErrorContext::new);
// 所有可能发生问题的地方都给你保存下来
private ErrorContext stored;
// 错误将来可能发生在哪个资源文件里面
private String resource;
// 错误是发生在哪个行为上(增删改查
private String activity;
// 哪个对象发生了错误
private String object;
// 你的异常消息是什么
private String message;
// 哪条sql语句发生了异常
private String sql;
private Throwable cause;
private ErrorContext() {
}
public static ErrorContext instance() {
return LOCAL.get();
}
// 该方法实现十分简单,内容可再源码中查看
}
里面有一个很重要的ThreadLocal变量LOCAL,他维护了一个当前线程的ErrorContext对象
ErrorContext负责封装异常上下文,而ExceptionFactory则负责输出异常上下文。
public class ExceptionFactory {
private ExceptionFactory() {
// Prevent Instantiation
}
public static RuntimeException wrapException(String message, Exception e) {
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}
}
我们可以使用源码来编写一个测试用例。
@Test
public void testErrorContext() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i {
try {
// mybatis对异常信息抽象了object、activity、sql等信心
ErrorContext.instance().object(this.getClass().getName());
ErrorContext.instance().activity("处理事件A中");
ErrorContext.instance().message("正在执行sql语句");
ErrorContext.instance().sql("select * * from user");
countDownLatch.countDown();
if (new Random().nextInt(10) > 6) {
int j = 1 / 0;
}
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
}
});
}
countDownLatch.await();
}