Оригинальный адрес блога:блог Пимайка
предисловие
Я использую MyBatis в качестве среды сохранения и знаю, что когда мы определяем класс интерфейса XXXMapper и используем его для операций CRUD, Mybatis использует технологию динамического прокси, чтобы помочь нам создавать классы прокси. Итак, каковы детали реализации внутри динамического прокси? Как связаны классы XXXMapper.java и XXXMapper.xml? В этой статье будет подробно проанализирован конкретный механизм реализации динамического прокси MyBatis.
Основные компоненты и приложения MyBatis
Прежде чем изучить динамический прокси-механизм в MyBatis подробно, давайте дополним основные знания и познакомиться с основными компонентами MyBatis.
- SqlSessionFactoryBuilder (конструктор): он может создавать SqlSessionFactory из XML, аннотаций или сконфигурированного вручную кода Java.
- SqlSessionFactory: Фабрика для создания SqlSession (сеанса)
- SqlSession: SqlSession — это основной класс Mybatis, который можно использовать для выполнения инструкций, фиксации или отката транзакций, а также для получения интерфейса преобразователя Mapper.
- SQL Mapper: он состоит из интерфейса Java и XML-файлов (или аннотаций), и необходимо указать соответствующие правила SQL и сопоставления.Он отвечает за отправку SQL для выполнения и возврат результата.
Примечание: сейчас мы используем Mybatis, который обычно интегрирован с фреймворком Spring, в этом случае SqlSession будет создаваться фреймворком Spring, поэтому нам часто не нужно использовать SqlSessionFactoryBuilder или SqlSessionFactory для создания SqlSessions.
Ниже показано, как использовать эти компоненты MyBatis или как быстро использовать MyBatis:
- Таблица базы данных
CREATE TABLE user(
id int,
name VARCHAR(255) not NULL ,
age int ,
PRIMARY KEY (id)
)ENGINE =INNODB DEFAULT CHARSET=utf8;
- объявить класс пользователя
@Data
public class User {
private int id;
private int age;
private String name;
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
- Определите глобальный файл конфигурации mybatis-config.xml (см. официальную документацию для объяснения конкретных тегов атрибутов в файле конфигурации)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--全局配置文件的根元素-->
<configuration>
<!--enviroments表示环境配置,可以配置成开发环境(development)、测试环境(test)、生产环境(production)等-->
<environments default="development">
<environment id="development">
<!--transactionManager: 事务管理器,属性type只有两个取值:JDBC和MANAGED-->
<transactionManager type="MANAGED" />
<!--dataSource: 数据源配置-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<!--mappers文件路径配置-->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
- Интерфейс UserMapper
public interface UserMapper {
User selectById(int id);
}
- Файл UserMapper
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace属性表示命令空间,不同xml映射文件namespace必须不同-->
<mapper namespace="com.pjmike.mybatis.UserMapper">
<select id="selectById" parameterType="int"
resultType="com.pjmike.mybatis.User">
SELECT id,name,age FROM user where id= #{id}
</select>
</mapper>
- тестовый класс
public class MybatisTest {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1);
System.out.println("User : " + user);
}
}
}
// 结果:
User : User{id=1, age=21, name='pjmike'}
Приведенный выше пример просто показывает, как использовать MyBatis, в то же время я буду использовать этот пример для дальнейшего изучения реализации динамического принципа MyBatis.
Реализация динамического прокси MyBatis
public static void main(String[] args) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// <1>
User user = userMapper.selectById(1);
System.out.println("User : " + user);
}
}
В предыдущем примере мы использовали метод sqlSession.getMapper() для получения объекта UserMapper, здесь мы фактически получили прокси-класс интерфейса UserMapper, а затем прокси-класс выполняет метод. Так как же генерируется этот прокси-класс? Прежде чем исследовать, как создается динамический прокси-класс, давайте взглянем на подготовку к процессу создания фабрики SqlSessionFactory, например, как читается файл конфигурации mybatis-config и как читается файл сопоставления?
Разбор файла глобальной конфигурации Mybatis
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
Мы используем новый метод SqlSessionFactoryBuilder().build() для создания фабрики SqlSessionFactory и ввода метода сборки.
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
Для распределения глобального файла конфигурации MyBatis соответствующий анализ код расположен в методе Parse () XMLConfigBuilderсередина:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//解析全局配置文件
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(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"));
//解析mapper映射器文件
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
Из исходного кода метода parseConfiguration легко увидеть, что он анализирует свойства каждого элемента в файле глобальной конфигурации mybatis. Конечно, после окончательного анализа возвращается объект Configuration. Configuration — очень важный класс. Он содержит всю информацию о конфигурации Mybatis. Он создается путем снятия денег с XMLConfigBuilder. Mybatis считывает информацию, настроенную в mybatis-config.xml, через XMLConfigBuilder, затем сохраните эту информацию в Configuration
Разбор картографических файлов
//解析mapper映射器文件
mapperElement(root.evalNode("mappers"));
Этот метод заключается в анализе атрибута mappers в глобальном файле конфигурации, перейдите:
mapperParser.parse()
Метод заключается в том, что XMLMapperBuilder анализирует файл сопоставления Mapper, который можно сравнить с XMLConfigBuilder.
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper")); //解析映射文件的根节点mapper元素
configuration.addLoadedResource(resource);
bindMapperForNamespace(); //重点方法,这个方法内部会根据namespace属性值,生成动态代理类
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
- Метод configurationElement (контекст XNode)
Этот метод в основном используется для помещения информации об элементе в файл сопоставления, напримерinsert
,select
Эта информация проанализирована в объект MapapeTatement и сохраняется в свойство Mapaptatements в классе конфигурации, чтобы последующий динамический прокси-класс может получить реальную информацию о выписке SQL при выполнении операций CRUD.
Для разбора используется метод buildStatementFromContext.insert、select
Этот тип информации об элементе инкапсулирован в объект MappedStatement Конкретные детали реализации здесь не приводятся.
- Метод bindMapperForNamespace()
Этот метод является основным методом. Он будет генерировать динамический прокси-класс для интерфейса в соответствии со значением атрибута пространства имен в файле сопоставления. Это относится к содержанию нашей темы — как генерируется динамический прокси-класс.
Генерация динамических прокси-классов
Исходный код метода bindMapperForNamespace выглядит следующим образом:
private void bindMapperForNamespace() {
//获取mapper元素的namespace属性值
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 获取namespace属性值对应的Class对象
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//如果没有这个类,则直接忽略,这是因为namespace属性值只需要唯一即可,并不一定对应一个XXXMapper接口
//没有XXXMapper接口的时候,我们可以直接使用SqlSession来进行增删改查
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
//如果namespace属性值有对应的Java类,调用Configuration的addMapper方法,将其添加到MapperRegistry中
configuration.addMapper(boundType);
}
}
}
}
Здесь упоминается метод конфигурации addMapper. Фактически, класс конфигурации поддерживает всю информацию об интерфейсе XxxMapper для создания динамических прокси-классов через объект MapperRegistry. Видно, что класс Configuration действительно является очень важным классом.
public class Configuration {
...
protected MapperRegistry mapperRegistry = new MapperRegistry(this);
...
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
...
}
Два важных метода: getMapper() и addMapper().
- getMapper(): динамический класс для создания интерфейсов.
- addMapper(): Когда mybatis анализирует файл конфигурации, он регистрирует интерфейс, который должен создать в нем динамический прокси-класс.
1. Configuration#addMappper()
Конфигурация делегирует метод addMapper в addMapper из MapperRegistry.Исходный код выглядит следующим образом:
public <T> void addMapper(Class<T> type) {
// 这个class必须是一个接口,因为是使用JDK动态代理,所以需要是接口,否则不会针对其生成动态代理
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 生成一个MapperProxyFactory,用于之后生成动态代理类
knownMappers.put(type, new MapperProxyFactory<>(type));
//以下代码片段用于解析我们定义的XxxMapper接口里面使用的注解,这主要是处理不使用xml映射文件的情况
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
MapperRegistry поддерживает внутренние отношения сопоставления, и каждый интерфейс соответствует MapperProxyFactory (генерирует класс динамической прокси-фабрики).
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
Это упрощает получение класса фабрики динамического прокси, соответствующего интерфейсу, непосредственно из Map при вызове getMapper() MapperRegistry позже, а затем использование класса фабрики для создания реального класса динамического прокси для его интерфейса.
2. Configuration#getMapper()
Метод GetMapper() конфигурации называется методом getMapper() MapPerRegistry, а исходный код выглядит следующим образом:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//根据Class对象获取创建动态代理的工厂对象MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//这里可以看到每次调用都会创建一个新的代理对象返回
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
Как видно из вышеизложенного, основной код для создания динамического прокси-класса находится в методе MapperProxyFactory.newInstance, исходный код выглядит следующим образом:
protected T newInstance(MapperProxy<T> mapperProxy) {
//这里使用JDK动态代理,通过Proxy.newProxyInstance生成动态代理类
// newProxyInstance的参数:类加载器、接口类、InvocationHandler接口实现类
// 动态代理可以将所有接口的调用重定向到调用处理器InvocationHandler,调用它的invoke方法
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
PS: Подробное введение в динамический прокси JDK здесь не будет, если вам интересно, вы можете обратиться к статье, которую я написал ранее:Принцип и применение динамического агента
Классом реализации интерфейса InvocationHandler здесь является MapperProxy, и его исходный код выглядит следующим образом:
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//如果调用的是Object类中定义的方法,直接通过反射调用即可
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//调用XxxMapper接口自定义的方法,进行代理
//首先将当前被调用的方法Method构造成一个MapperMethod对象,然后掉用其execute方法真正的开始执行。
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
...
}
Окончательная логика выполнения заключается в методе execute класса MapperMethod.Исходный код выглядит следующим образом:
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
//insert语句的处理逻辑
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
//update语句的处理逻辑
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
//delete语句的处理逻辑
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
//select语句的处理逻辑
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
//调用sqlSession的selectOne方法
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
...
}
В MapperMethod также есть два внутренних класса, SqlCommand и MethodSignature.В методе выполнения оператор case switch используется для определения типа SQL, который должен выполняться в соответствии с методом getType() SqlCommand, например INSET, UPDATE, DELETE. , SELECT и FLUSH, затем вызовите методы добавления, удаления, изменения и проверки SqlSession соответственно.
Подождите, так много сказано, тогда этот метод getMapper() вызывается, когда он? Вызываем начало фактически SqlSession метода getMapper():
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
...
}
Таким образом, приблизительная логическая цепочка вызова метода getMapper выглядит так: SqlSession#getMapper() ——> Configuration#getMapper() ——> MapperRegistry#getMapper() ——> MapperProxyFactory#newInstance() ——> Proxy#newProxyInstance()
Еще одна вещь, на которую мы должны обратить внимание:Мы получаем интерфейсный прокси через метод getMapper SqlSession для выполнения операций CRUD, а нижний уровень по-прежнему зависит от использования SqlSession..
резюме
Согласно вышеуказанному процессу разведки, логическая диаграмма просто нарисована (не обязательно точна):
В этой статье в основном вводит динамический принцип MyBatis. Если нам нужно знать, как мы используем динамичный прокси-класс Usermapper для выполнения операций CRUD, по сути, ключевой класс SQLSession выполняется, но как выполнить Crudi для SQLSession. Тщательно продумано, и заинтересованные студенты могут проверить соответствующую информацию.