Если мы будем использовать MyBatis в виде простого кода, наш код будет выглядеть примерно так:
// Определение основного конфигурационного файла MyBatis
String resource = "resources/config-custom-s1.xml";
// Получение входного потока конфигурационного файла MyBatis
InputStream inputStream = Test.class.getClassLoader().getResourceAsStream(resource);
// Создание объекта SqlSessionFactory с помощью входного потока
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// Получение объекта SqlSession с помощью SqlSessionFactory
SqlSession sqlSession = sqlSessionFactory.openSession();
// Выполнение операций
// Методы userDao будут заменены на прокси
UserDao userDao = sqlSession.getMapper(UserDao.class);
User teacher = userDao.getById(1);
System.out.println(userDao.insert(2, "panda"));
sqlSession.rollback();
// Закрытие сессии
sqlSession.close();
Сначала мы инициализируем объект SqlSessionFactory с помощью основного конфигурационного файла MyBatis, затем используем этот объект SqlSessionFactory для получения объекта SqlSession, и в этой сессии получаем интерфейс, который мы хотим использовать для выполнения операций DAO.
Прежде чем углубляться в детали реализации, давайте сначала познакомимся с тремя основными классами:
Из названия класса SqlSessionFactory можно легко догадаться, что это фабрика для создания объектов SqlSession. Когда мы говорим о фабриках, мы имеем в виду паттерн проектирования фабрики, который позволяет использовать методы фабрики вместо обычного создания объектов с помощью оператора new. Это также может упростить некоторые процессы создания, хотя здесь мы только коснемся этого паттерна, не углубляясь в детали, так как изучение исходного кода MyBatis — это наш основной приоритет.
Объект SqlSession создается с помощью SqlSessionFactory. Этот объект SqlSession является первым настоящим объектом, который нам нужно изучить.
Мы знаем, что для выполнения операций с базой данных нам сначала нужно получить соединение с базой данных, а затем уже выполнять операции.
В фреймворке MyBatis операции с JDBC были упакованы и унифицированы, и мы можем рассматривать этот API как объект Connection в MyBatis. Конкретная форма этого API — это объект SqlSession.Однако, в отличие от Connection
, объект SqlSession не только упаковывает операции с JDBC, но и предоставляет другие полезные функции, такие как метод SqlSession#getMapper()
, который принимает тип интерфейса DAO и возвращает соответствующий объект. При вызове методов этого объекта автоматически выполняются операции с JDBC.Как реализуется эта функция? Это связано с основными принципами работы Mybatis, но вкратце можно сказать, что реализация основана на паттерне проектирования прокси. Этот паттерн является одним из часто используемых паттернов проектирования, который позволяет заменить объект-цель и выполнять операции вместо него. В случае с JDBC, определенные операции DAO фактически не существуют, но благодаря прокси-объекту они выглядят как экземпляры интерфейсов DAO и выполняют операции с атрибутами JDBC.
Но когда Mybatis создает прокси для интерфейсов DAO? Ответ на этот вопрос мы найдем, вернувшись к объекту SqlSessionFactoryBuilder
и рассмотрев его.
SqlSessionFactoryBuilder
кажется простым интерфейсом, который определяет метод build
с несколькими перегрузками.
SqlSessionFactory build(Reader reader);
SqlSessionFactory build(Reader reader, String environment);
SqlSessionFactory build(Reader reader, Properties properties);
SqlSessionFactory build(Reader reader, String environment, Properties properties);
SqlSessionFactory build(InputStream inputStream);
SqlSessionFactory build(InputStream inputStream, String environment);
SqlSessionFactory build(InputStream inputStream, Properties properties);
SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);
SqlSessionFactory build(Configuration config);
Перегруженные методы SqlSessionFactory
самые сложные из двух.
SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);
SqlSessionFactory build(Reader reader, String environment, Properties properties);
Эти два метода очень похожи, различие заключается только в конкретном типе потока файлов: один использует Reader, другой — InputStream.
Основное различие отсутствует, поэтому здесь мы рассмотрим метод build, соответствующий InputStream. К слову, мы можем преобразовать InputStream в Reader с помощью Reader reader = new InputStreamReader(inputStream);
. /**
* Создает объект SqlSessionFactory
*
* @param inputStream байтовый поток конфигурационного файла Mybatis
* @param environment имя по умолчанию для окружения, конфигурация окружения может быть найдена в узле Environments конфигурационного файла Mybatis, по умолчанию "default"
* @param properties определенные параметры
*/
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// Создает парсер основного конфигурационного файла Mybatis
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// Использует по умолчанию JDBC сессионный фабрику
return build(
parser.parse()/*Парсит конфигурационный файл Mybatis*/
);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Ошибка при создании SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Намеренно игнорируем. Предпочтение отдается предыдущей ошибке.
}
}
}
В методе build создается объект XMLConfigBuilder, который фактически является конкретной реализацией абстрактного класса BaseBuilder. Основная цель этого объекта — парсить основной конфигурационный файл Mybatis.С точки зрения определения, BaseBuilder должен был быть предназначен для унификации входа в парсинг и построение компонентов Mybatis, но в реальности его конкретная реализация не следует этой концепции. Кроме того, я не очень люблю название *Builder, так как оно звучит как название для построения сложного объекта, но на самом деле он больше занимается парсингом XML-конфигураций, чем построением объектов. Поэтому я предпочитаю названия вроде *Parser, которые используются только для парсинга, а конкретные операции построения доверены *Register. Конечно, это только мое личное мнение.
Продолжая вернуться к коду, когда мы вызываем метод XMLConfigBuilder#parse()
, он возвращает объект Configuration
, который передаётся методу SqlSessionFactory build(Configuration config)
для дальнейшей обработки. Ну что ж, вышло так, что конечная реализация заключается в вызове одноаргументного метода.
/**
* Создаёт объект SqlSessionFactory
*
* @param config конфигурационный объект Mybatis
*/
public SqlSessionFactory build(Configuration config) {
// Создаётся объект DefaultSqlSessionFactory
return new DefaultSqlSessionFactory(config);
}
Этот метод довольно прост, он просто создаёт экземпляр DefaultSqlSessionFactory
с помощью объекта Configuration
.Теперь, пропустив метод SqlSessionFactory build(Configuration config)
, мы снова обращаемся к XmlConfigBuilder
:
// Создаётся парсер конфигурационного файла Mybatis
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// Используется по умолчанию JDBC-сессионный фабрикатор
return build(
parser.parse() /*Парсинг конфигурационного файла Mybatis*/
);
Здесь вызывается конструктор XMLConfigBuilder(InputStream inputStream, String environment, Properties props)
для инициализации экземпляра XmlConfigBuilder
, а затем используется метод #parse
для создания объекта Configuration
, который используется для конфигурации экземпляра DefaultSqlSessionFactory
.Очевидно, что в методе XmlConfigBuilder#parse()
выполняются некоторые неизвестные нам методы, которые используются для парсинга XML-конфигурационного файла Mybatis и создания объекта конфигурации Mybatis.
Отслеживая код в конструкторе XMLConfigBuilder# XMLConfigBuilder(InputStream inputStream, String environment, Properties props)
:
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(
inputStream, /*Входной поток XML-файла*/
true, /*Включение валидации*/
props, /*Свойства*/
new XMLMapperEntityResolver() /*Фабрика XML-сущностей*/
) /*Создание экземпляра парсера XML*/
, environment /*Объект окружения*/
, props /*Свойства*/
);
}
Вот MyBatis использует входной поток XML-файла для создания объекта XPathParser
. Функция XPathParser
довольно проста: он использует SAX
для парсинга XML-документа и создания DOM-дерева, которое затем сохраняется в объекте XPathParser
. Ниже приведены некоторые части кода класса XPathParser
:
public class XPathParser {
/**
* Содержимое XML-документа
*/
private final Document document;
/**
* Проверка DTD
*/
private boolean validation;
/**
* Резолвер сущностей
*/
private EntityResolver entityResolver;
/**
* Свойства
*/
private Properties variables;
/**
* Объект XPath
*/
private XPath xpath;
}
Конструктор, который мы только что вызвали, выглядит следующим образом:
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(reader));
}
```В этом конструкторе есть две части: вызов `commonConstructor` для инициализации текущих свойств и вызов `createDocument` для парсинга DOM-дерева, соответствующего XML-файлу.
Конструктор `commonConstructor` довольно прост, его код представлен ниже:```java
/**
* Общий конструктор для инициализации свойств
*
* @param validation Проверка DTD
* @param variables Свойства
* @param entityResolver Резолвер сущностей
*/
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
// Проверка DTD
this.validation = validation;
// Конфигурация резолвера сущностей
this.entityResolver = entityResolver;
// Свойства MyBatis
this.variables = variables;
}
``````md
## Инициализация XPath
```java
// Инициализация XPath
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
### Метод `createDocument`
Метод `createDocument`主要用于根据当前配置解析XML文件并获取DOM对象:
```java
/**
* Создает текстовый объект на основе входного источника.
*
* @param inputSource входной источник
* @return текстовый объект
*/
private Document createDocument(InputSource inputSource) {
// важно: это должно быть вызвано ТОЛЬКО после общего конструктора
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// проверка, должна ли быть валидация документа, который будет анализироваться
factory.setValidating(validation);
// поддержка XML-пространств имен
factory.setNamespaceAware(false);
// игнорирование комментариев
factory.setIgnoringComments(true);
// игнорирование пробельных символов
factory.setIgnoringElementContentWhitespace(false);
// преобразование узлов CDATA в узлы TEXT
factory.setCoalescing(false);
// разворачивание узлов с ссылками на сущности
factory.setExpandEntityReferences(true);
}
``` DocumentBuilder builder = factory.newDocumentBuilder();
// Настройка парсера XML-документа, в основном это XMLMapperEntityResolver
builder.setEntityResolver(entityResolver);
``````java
setEntityResolver(entityResolver);
// Настройка обработчика ошибок
builder.setErrorHandler(new ErrorHandler() {
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
});
``` @Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
}
});
// Парсинг DOM-дерева
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Ошибка при создании экземпляра документа. Причина: " + e, e);
}
}
После выполнения этих двух методов процесс конструирования XPathParse
завершается. В этот момент он уже содержит объект Document
и доступ к XPath
-представлению, и последующие операции будут использовать XPath
для чтения содержимого объекта Document
.
Вернемся к конструктору XMLConfigBuilder
. Мы уже упоминали, что объект XMLConfigBuilder
является подклассом BaseBuilder
. В классе BaseBuilder
есть три final
-атрибута:
public abstract class BaseBuilder {
/**
* Конфигурация MyBatis
*/
protected final Configuration configuration;
/**
* Регистр типовых псевдонимов
*/
protected final TypeAliasRegistry typeAliasRegistry;
/**
* Регистр типовых обработчиков
*/
protected final TypeHandlerRegistry typeHandlerRegistry;
Атрибут Configuration
представляет конфигурацию MyBatis, в которой хранятся различные настройки для использования MyBatis в процессе работы. Мы не будем подробно останавливаться на этом сейчас, а рассмотрим его при конкретном использовании.- Атрибут TypeAliasRegistry
представляет регистр типовых псевдонимов Mybatis, который используется для упрощения кода при работе с Mybatis. Например, при определении resultMap
можно использовать <result ... javaType="int"/>
, вместо полного имени класса java.lang.Integer
. Реализация TypeAliasRegistry
сама по себе очень проста. Она поддерживает MAP-коллекцию, названную Map<String, Class<?>> TYPE_ALIASES
, которая используется для хранения отображения между именами классов и их определениями. При этом в конструкторе по умолчанию регистрируются некоторые часто используемые альтернативные названия классов.
Атрибут TypeHandlerRegistry
содержит регистр типовых обработчиков преобразования типов Mybatis. Типовые обработчики (TypeHandler
) предназначены для выполнения преобразований между Java типами и JDBC типами, а также инициализируются некоторыми часто используемыми типовыми обработчиками при создании.XMLConfigBuilder
также определяет несколько свойств, которые выполняют следующие функции:
/*Флаг парсинга, предотвращающий повторное парсинг*/
private boolean parsed;
/*XML адрес парсера*/
private final XPathParser parser;
/*Указывает текущую среду выполнения Mybatis*/
private String environment;
Здесь свойство environment
используется для указания текущей среды выполнения Mybatis, что будет подробно объяснено в дальнейшем процессе парсинга.
```Возвращаясь к конструктору XMLConfigBuilder
, после инициализации объекта `XPathParser`, вызывается перегруженный конструктор `XMLConfigBuilder(XPathParser parser, String environment, Properties props)`:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// Инициализация объекта Configuration и регистрация некоторых псевдонимов и языковых драйверов
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
// Установка переменных MyBatis
this.configuration.setVariables(props);
// Инициализация флага парсинга
this.parsed = false;
// Инициализация среды выполнения
this.environment = environment;
// Инициализация парсера XML адреса
this.parser = parser;
}
Важно отметить, что в этом конструкторе вызывается конструктор родительского класса:
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
// Синхронизация таблицы псевдонимов из конфигурации MyBatis
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
// Синхронизация таблицы типовых обработчиков из конфигурации MyBatis
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
Из кода видно, что BaseBuilder#typeAliasRegistry
и BaseBuilder#typeHandlerRegistry
ссылаются на свойства из конфигурации MyBatis. Здесь стоит обратить внимание:> BaseBuilder#typeAliasRegistry
и Configuration#typeAliasRegistry
являются одним и тем же экземпляром.
BaseBuilder#typeHandlerRegistry
иConfiguration#typeHandlerRegistry
являются одним и тем же экземпляром. До этого момента процесс инициализацииXMLConfigBuilder
был полностью завершен.
Далее MyBatis вызывает метод XmlConfigBuilder#parse()
в позиции SqlSessionFactoryBuilder#build()
>return build(parser.parse()/*парсинг конфигурации MyBatis*/);
, что запускает весь процесс инициализации и парсинга MyBatis.
public Configuration parse() {
if (parsed) {
// Второй вызов XMLConfigBuilder
throw new BuilderException("Каждый экземпляр XMLConfigBuilder может быть использован только один раз.");
}
// Сброс флага парсинга XMLConfigBuilder для предотвращения повторного парсинга
parsed = true;
// Начало парсинга конфигурационного файла MyBatis
// Парсинг конфигурационного файла, чтение содержимого узла [configuration]
parseConfiguration(parser.evalNode("/configuration"));
// Возврат экземпляра конфигурации MyBatis
return configuration;
}
Сначала вызывается метод #parseConfiguration()
для парсинга узла configuration
основного конфигурационного файла MyBatis, а данные узла configuration
получены с помощью parser.evalNode("/configuration")
, что в конечном итоге приводит к методу:
private Object evaluate(String expression, Object root, QName returnType) {
try {
// Вычисление XPath-выражения в указанном контексте и возврат результата в виде указанного типа.
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Ошибка вычисления XPath. Причина: " + e, e);
}
}
```Затем объект передается методом `return new XNode(this, node, variables);` для упаковки в объект типа `XNode`.
Основные определения класса `XNode` следующие:
public class XNode { /**
}
Основная роль этого класса заключается в упрощении доступа к ресурсам DOM.
После упаковки содержимого узла `configuration` в объект типа `XNode`, начинается настоящий процесс парсинга.## Анализ основного конфигурационного файла MyBatis и настройка MyBatis для создания необходимой среды
`configuration` является корневым узлом основного конфигурационного файла MyBatis, и мы обычно используем его следующим образом:
```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>
. . .
</configuration>
Ссылаясь на DTD-определение MyBatis, можно заметить, что в узле configuration
допускаются 11 типов дочерних узлов, и все они являются необязательными. Это означает, что MyBatis имеет по умолчанию обработку для этих свойств:
<! ELEMENT configuration (
properties?
, settings?
, typeAliases?
, typeHandlers?
, objectFactory?
, objectWrapperFactory?
, reflectorFactory?
, plugins?
, environments?
, databaseIdProvider?
, mappers?
)>
Каждый из этих дочерних узлов имеет определенное назначение, которое мы рассмотрим подробнее в процессе анализа. Продолжим анализ метода parseConfiguration
:
/**
* Разбор узла Configuration
*/
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// Загрузка конфигурационных файлов и перезапись соответствующих свойств [узел properties]
propertiesElement(root.evalNode("properties"));
// Преобразование содержимого узла settings в Property и проверка
Properties settings = settingsAsProperties(root.evalNode("settings"));
// Определение способа доступа к файлам ресурсов на основе конфигурации settings
loadCustomVfs(settings);
// Определение способа обработки логов на основе конфигурации settings
loadCustomLogImpl(settings);
// Разбор псевдонимов
typeAliasesElement(root.evalNode("typeAliases"));
// Конфигурация плагинов
pluginElement(root.evalNode("plugins"));
// Создание фабрики для создания объектов
objectFactoryElement(root.evalNode("objectFactory"));
// Создание фабрики для обертывания объектов
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// Создание фабрики для рефлексии
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// Инициализация глобальной конфигурации на основе конфигурации settings
settingsElement(settings);
// Чтение конфигурации среды и поиск соответствующего менеджера транзакций и источника данных для текущей среды (по умолчанию default)
environmentsElement(root.evalNode("environments"));
// Создание класса для маркера типа базы данных, MyBatis загружает все заявки без databaseId и с текущим databaseId, заявки с databaseId имеют более высокий приоритет
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// Регистрация конвертеров типов
typeHandlerElement(root.evalNode("typeHandlers"));
// Регистрация парсинга файлов MapperXml, соответствующих Dao
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Ошибка при разборе конфигурации SQL Mapper. Причина: " + e, e);
}
}
В процессе разбора сначала загружается узел configuration -> properties
, и соответствующие свойства перезаписываются в конфигурации MyBatis. Определение свойств properties очень простое:
<!ELEMENT properties (property*)>
<!ATTLIST properties
resource CDATA #IMPLIED
url CDATA #IMPLIED
>
Оно позволяет иметь несколько элементов property
, а также настраивать атрибуты resource
и url
. Однако, атрибуты resource
и url
не могут существовать одновременно.
Определение элемента property
выглядит следующим образом:
<!ELEMENT property EMPTY>
<!ATTLIST property
name CDATA #REQUIRED
value CDATA #REQUIRED
>
```Элемент `property` состоит из `name` и `value`, поэтому его можно интерпретировать как свойство.
В процессе конкретного анализа можно заметить, что Mybatis предоставляет соответствующие операции анализа для этих трех различных методов конфигурации.
/**
// Загрузка файла конфигурации *. Properties
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
// Загрузка файла конфигурации *. Properties
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
// Загрузка конфигурационных данных
defaults.putAll(vars);
}
// Обновление конфигурационных данных и синхронизация глобальных значений по умолчанию в парсер
parser.setVariables(defaults);
// Синхронизация в настройках MyBatis
configuration.setVariables(defaults);
}
}
MyBatis сначала получает все элементы property
из секции properties
и на их основе создает объект Properties
. Реализация метода context.getChildrenAsProperties();
выглядит следующим образом:
/**
* Получение всех элементов settings из секции settings, где setting>name является ключом для Properties,
* а setting>value — значением.
*/
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
for (XNode child : getChildren()) {
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
properties.setProperty(name, value);
}
}
return properties;
}
Затем MyBatis получает атрибуты resource
и url
из секции properties
, проверяет, чтобы они не были указаны одновременно, и загружает соответствующий файл конфигурации на основе указанных атрибутов resource
или url
. Затем загруженные данные сохраняются в объект Properties
.Затем MyBatis получает все переменные из конфигурационного объекта Configuration
и сохраняет их в объект Properties
. В конце концов, с использованием объединенного объекта Properties
обновляются конфигурационные данные в парсере XmlPathParser
и в конфигурационном объекте Configuration
.С учетом вышеописанного порядка загрузки можно понять, что при конфигурации свойств MyBatis приоритеты следующие:
url
или resource
в основном конфигурационном файле MyBatisproperty
в основном конфигурационном файле MyBatis.### Параметры конфигурации MybatisПосле парсинга узла properties
, следует парсинг узла settings
.
// Преобразование содержимого узла settings в Property и проверка.
Properties settings = settingsAsProperties(root.evalNode("settings"));
Определение узла settings
:
<!ELEMENT setting EMPTY>
<!ATTLIST setting
name CDATA #REQUIRED
value CDATA #REQUIRED
>
Это определяет, что в узле settings
должны быть один или более узлов setting
, каждый из которых должен иметь два параметра: name
и value
. Узел setting
не может иметь дополнительных дочерних узлов.
Из этого DTD также следует, что узел settings
будет преобразован в объект Properties
.
Код для парсинга settings
:
/**
* Парсинг узла settings
*
* @param context узел settings
*/
private Properties settingsAsProperties(XNode context) {
if (context == null) {
// Если узел settings не указан, вернуть пустой объект Properties
return new Properties();
}
Properties props = context.getChildrenAsProperties();
}
``` // Проверка, что все параметры settings поддерживаются конфигурацией
// Проверка, что все параметры settings известны классу Configuration
// Получение объекта MetaClass для класса Configuration
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
// Проверка поддержки параметров settings
throw new BuilderException("Параметр конфигурации " + key + " неизвестен. Убедитесь, что вы правильно его написали (чувствительность к регистру).");
}
}
return props;
}При парсинге узла `settings` выполняется несколько действий. Сначала весь узел `settings` преобразуется в объект `Properties`. Если узел `settings` не указан, возвращается пустой объект `Properties`. Если узел `settings` указан, проверяется, что все параметры поддерживаются конфигурацией.
В этом процессе используется новый объект `MetaClass`, который Mybatis использует для хранения метаданных типа объекта. У этого объекта есть два основных атрибута: фабрика рефлексии и объект, описывающий метаданные атрибутов.
```java
public class MetaClass {
/**
* Рефлексный фабрикатор
*/
private final ReflectorFactory reflectorFactory;
/**
* Метаданные для заданного класса
*/
private final Reflector reflector;
}
В данном контексте ReflectorFactory
используется для чтения class
и генерации Reflector
, который является вспомогательным фабрикатором. Reflector
кэширует базовую информацию о классе, включая тип класса, имена читаемых и записываемых свойств, а также соответствующие методы getter
и setter
, конструкторы и т.д.
В этом случае XmlConfigBuilder
использует DefaultReflectorFactory
для получения базовых свойств Configuration
и проверяет, есть ли соответствующие методы setter
для свойств конфигурации settings
.Таким образом, весь процесс анализа settings
завершен.
Дальнейшие действия заключаются в определении способа доступа MyBatis к ресурсам внутри JAR-файла на основе конфигурации settings
. В MyBatis определён абстрактный класс VFS
, который задаёт API для доступа к ресурсам контейнера. По умолчанию имеются два реализации этого класса: JBoss6VFS.class
и DefaultVFS.class
.```java
// Определение способа доступа к ресурсам на основе конфигурации settings
loadCustom_vfs(settings);
```java
/**
* Загрузка реализации класса для доступа к системному виртуальному файловому системе
*
* @param props глобальные системные настройки
* @throws ClassNotFoundException если реализация класса не найдена
*/
private void loadCustom_vfs(Properties props) throws ClassNotFoundException {
// Получение настроек vfsImpl из props, если они существуют, они перекрывают стандартные реализации
String value = props.getProperty("vfsImpl");
if (value != null) {
String[] clazzes = value.split(",");
for (String clazz : clazzes) {
if (!clazz.isEmpty()) {
@SuppressWarnings("unchecked")
Class<? extends VFS> vfsImpl = (Class<? extends VFS>) Resources.classForName(clazz);
// Обновление реализации доступа к виртуальной файловой системе MyBatis
configuration.setVfsImpl(vfsImpl);
}
}
}
}
В приведенном выше коде метод configuration.setVfsImpl(vfsImpl)
не просто устанавливает значение, но и вызывает метод VFS.addImplClass(this.vfsImpl)
:
/**
* Установка нового класса VFS
* @param vfsImpl класс VFS
*/
public void setVfsImpl(Class<? extends VFS> vfsImpl) {
if (vfsImpl != null) {
this.vfsImpl = vfsImpl;
// Добавление нового класса VFS
VFS.addImplClass(this.vfsImpl);
}
}
```Метод `VFS.addImplClass(this.vfsImpl)` добавляет новый класс реализации VFS в `VFS#USER_IMPLEMENTATIONS`, что позволяет при получении экземпляра VFS использовать этот новый класс в приоритетном порядке. После завершения обновления класса реализации Mybatis для доступа к виртуальной файловой системе, продолжается конфигурация класса реализации логирования Mybatis в соответствии с параметрами `settings`.
// Определяет метод обработки логирования в зависимости от настроек loadCustomLogImpl(settings);
/**
Только после этого завершается обработка параметров `settings` на данном этапе.
### Разбор механизма псевдонимов типов Mybatis (узел typeAliases)
Следующим этапом является обработка регистрации псевдонимов типов Mybatis. Ранее было упомянуто, что псевдонимы типов используются для упрощения кода при использовании Mybatis.Для настройки своих псевдонимов в MyBatis используются элементы `typeAliases`, определение которых представлено ниже:
В элементе `typeAliases` могут быть указаны ноль или более элементов `typeAlias`/`package`, которые не могут содержать дополнительные подэлементы. Вот результат перевода:
В разделе `typeAliases`:
- Внутри тега `typeAlias` можно указать два параметра: `type` и `alias`. Параметр `type` должен указывать полное имя типа Java, и он является обязательным. Параметр `alias` представляет собой псевдоним для объекта Java и является необязательным. По умолчанию используется имя класса Java, полученное с помощью `#getSimpleName()`.
- Внутри тега `package` можно указать один обязательный параметр `name`, который должен указывать имя пакета Java. Все классы в этом пакете, соответствующие определенным правилам (по умолчанию это подклассы `Object.class`), будут зарегистрированы.```java
/**
* Парсинг конфигурации для узла typeAliases
*
* @param parent узел typeAliases
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// Регистрация псевдонимов для пакета, по умолчанию используется имя класса Java
String typeAliasPackage = child.getStringAttribute("name");
// Регистрация псевдонимов в таблице псевдонимов
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// Обработка конфигурации typeAlias и регистрация псевдонимов и типов в таблице псевдонимов
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Ошибка при регистрации typeAlias для '" + alias + "'. Причина: " + e, e);
}
}
}
}
}
```MyBatis обрабатывает узел `typeAliases` двумя способами: обработка узла `package` и обработка узла `typeAlias`. Обработка пакета начинается с получения базового имени пакета через свойство `name` у объекта `package`. Затем вызывается метод `configuration#getTypeAliasRegistry()#registerAliases()` для регистрации псевдонимов.
> Важно отметить, что ранее было упомянуто, что на самом деле `typeAliasRegistry` в `XmlConfigBuilder` и `configuration` указывают на один и тот же экземпляр. Поэтому вызовы `typeAliasRegistry#registerAliases()` и `configuration#getTypeAliasRegistry()#registerAliases()` не приводят к实质性区别的结果。
Далее рассмотрим метод `TypeAliasRegistry#registerAliases(String)`, который предоставляет возможность массовой регистрации псевдонимов для классов.
/** * Регистрация всех Java-классов в указанном пакете * * @param packageName Имя пакета */ public void registerAliases(String packageName) { registerAliases(packageName, Object.class); }
Вызов переопределенного метода с передачей ограничивающего типа, по умолчанию `Object.class`. Это означает, что будут зарегистрированы псевдонимы для всех подклассов `Object.class` в данном пакете.```
/**
* Регистрация псевдонимов для указанного типа и его подтипов
*
* @param packageName Имя пакета
* @param superType Ограничивающий тип
*/
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// Возвращение текущего набора найденных классов
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for (Class<?> type : typeSet) {
// Игнорирование вложенных классов, интерфейсов (включая package-info.java)
// Также пропускаем анонимные классы, интерфейсы и вложенные классы. См. проблему #6
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
// Регистрация псевдонима
registerAlias(type);
}
}
}
В этом методе используется ResolverUtils#find()
для получения набора классов, соответствующих ожиданиям. Затем из этого набора исключаются анонимные классы, интерфейсы и вложенные классы, и для оставшихся классов выполняется регистрация псевдонимов.> Метод ResolverUtils#find()
используется для рекурсивного поиска всех классов в указанном пакете и его подпакетах, а также для выполнения теста на соответствие каждому найденному классу. Только те классы, которые проходят тест, сохраняются. Код представлен ниже:
* Рекурсивно просматривает указанный пакет и его подпакеты для классов и выполняет тест Test для всех найденных классов. Только те классы, которые проходят тест, будут сохранены.
*
* @param test Объект теста для фильтрации классов
* @param packageName Базовое имя пакета для просмотра
*/
public ResolverUtil<T> find(Test test, String packageName) {
String path = getPackagePath(packageName);
try {
// Получает все файлы в указанном пути
List<String> children = VFS.getInstance().list(path);
for (String child : children) {
if (child.endsWith(".class")) {
// Обрабатывает все файлы компиляции классов
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Не удалось прочитать пакет: " + packageName, ioe);
}
return this;
}
В этом методе есть строка кода List<String> children = VFS.getInstance().list(path);
, где ранее упоминалось, что пользовательские экземпляры VFS становятся активными именно здесь.
/**
* Холдер для одиночки
* Singleton instance holder.
*/
private static class VFSHolder {
static final VFS INSTANCE = createVFS(); ```java
@SuppressWarnings("unchecked")
static VFS createVFS() {
// Сначала пробуем реализации учителя, затем встроенные
List<Class<? extends VFS>> impls = new ArrayList<>();
// Применяем пользовательские реализации
impls.addAll(USER_IMPLEMENTATIONS);
// Применяем системные реализации
impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));
}
``` // Пытаемся использовать каждую реализацию до тех пор, пока не найдется действительная
VFS vfs = null;
for (int i = 0; vfs == null || !vfs.isValid(); i++) {
// Завершаем, когда не удается получить объект vfs или находит действительный объект vfs
``````markdown
// Получить тип реализации vfs
Class<? extends VFS> impl = impls.get(i);
try {
// Создать экземпляр vfs
vfs = impl.newInstance();
if (vfs == null || !vfs.isValid()) {
if (log.isDebugEnabled()) {
log.debug("Реализация VFS " + impl.getName() +
" недействительна в этой среде.");
}
}
} catch (InstantiationException e) {
log.error("Не удалось создать экземпляр " + impl, e);
return null;
} catch (IllegalAccessException e) {
log.error("Не удалось создать экземпляр " + impl, e);
return null;
}
}
``````markdown
Процесс инстанцирования объекта `VFS` также использует паттерн проектирования — одиночку (singleton), хотя подробное понимание этого паттерна здесь не требуется.
Вернемся к методу разрешения псевдонимов. После того как все необходимые для регистрации псевдонимов классы из определенного пакета были найдены, MyBatis продолжает обрабатывать эти классы, передавая задачу регистрации псевдонимов методу `registerAlias(Class)`.```java
/**
* Регистрирует псевдоним для указанного типа в таблице псевдонимов.
* В случае отсутствия аннотаций используется имя класса в нижнем регистре.
*
* @param 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);
}
```В этом методе основное внимание уделено получению псевдонима для класса, который требуется зарегистрировать. По умолчанию псевдоним — это простое имя класса, полученное с помощью метода `#getSimpleName()`. Однако, если на классе найдена аннотация `Alias`, значение псевдонима из этой аннотации будет использовано вместо значения по умолчанию.
```После получения псевдонима продолжите вызов метода `registerAlias(String, Class<?>)` для выполнения операции регистрации псевдонима.
/**
* Регистрирует указанный тип и псевдоним в таблице псевдонимов
*
* @param alias псевдоним
* @param value указанный тип
*/
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("Параметр alias не может быть null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
// Проверка на наличие уже зарегистрированного псевдонима
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("Псевдоним '" + alias + "' уже связан со значением '" + TYPE_ALIASES.get(key).getName() + "'. ");
}
// Добавление псевдонима в таблицу псевдонимов
TYPE_ALIASES.put(key, value);
}
При регистрации псевдонима сначала преобразуется псевдоним в строку в нижнем регистре, затем проверяется, существует ли уже зарегистрированный псевдоним или псевдоним с таким же типом. Если ни один из них не существует, псевдоним добавляется в таблицу псевдонимов, то есть в TypeAliasRegistry#TYPE_ALIASES
.До этого момента парсинг подэлемента package
для тега typeAliases
завершён. Теперь перейдём к рассмотрению другого тега typeAlias
. Ранее было упомянуто, что typeAlias
имеет два атрибута: обязательный атрибут type
, который указывает полное имя класса Java, и необязательный атрибут alias
, который задаёт псевдоним для класса Java.На самом деле, парсинг тега typeAlias
уже включен в процесс парсинга тега package
. При парсинге тега typeAlias
сначала проверяется наличие класса Java, на который указывает атрибут type
. Если класс отсутствует, выбрасывается исключение. Если класс существует, то продолжается обработка значения атрибута alias
.
Если для typeAlias
задано значение атрибута alias
, то вызывается метод registerAlias(String, Class<?>)
для регистрации псевдонима. Если значение атрибута alias
не задано, то сначала вызывается метод registerAlias(Class)
, а затем метод registerAlias(String, Class<?>)
для регистрации псевдонима после его генерации.
Таким образом, парсинг узла typeAliases
завершен. Теперь перейдем к парсингу плагинов MyBatis.
plugins
)Плагины MyBatis — это мощная функциональность, которая позволяет вмешиваться в работу MyBatis во время выполнения. Это позволяет выполнять различные действия, такие как популярный плагин пагинации page-helper
, который основан на функциональности плагинов MyBatis.
DTD-определение конфигурации плагинов MyBatis выглядит следующим образом:
<!ELEMENT plugins (plugin+)>
<!ELEMENT plugin (property*)>
<!ATTLIST plugin
interceptor CDATA #REQUIRED
>
Тег plugins
должен содержать один или более тегов plugin
. Тег plugin
имеет обязательный атрибут interceptor
, который указывает на класс, реализующий интерфейс Interceptor
. Внутри тега plugin
могут быть указаны ноль или более тегов property
, которые задают свойства конфигурации для плагина.Интерфейс Interceptor
определен следующим образом:
/**
* Интерфейс MyBatis-плагина (интерцептора)
* @author Clinton Begin
*/
public interface Interceptor {
/**
* Метод для перехвата выполнения операции
* @param invocation вызов
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* Метод для перехвата процесса создания объекта
*/
Object plugin(Object target);
/**
* Метод для установки свойств интерцептора
*/
void setProperties(Properties properties);
}
Затем мы продолжаем анализ конфигурации плагинов Mybatis:
// Конфигурация плагинов
pluginElement(root.evalNode("plugins"));
/**
* Парсинг узла plugins
*
* @param parent узел plugins
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// Обработка каждого interceptor
String interceptor = child.getStringAttribute("interceptor");
// Получение отображения name=>value
Properties properties = child.getChildrenAsProperties();
// Получение реализации плагина
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
// Инициализация конфигурации плагина
interceptorInstance.setProperties(properties);
// Регистрация плагина, добавление нового экземпляра плагина в цепочку обязанностей плагинов
configuration.addInterceptor(interceptorInstance);
}
}
}
В процессе работы всего плагина есть два места, которые требуют внимания: один — (Interceptor) resolveClass(interceptor).newInstance();
, второй — configuration.addInterceptor(interceptorInstance);
.Метод resolveClass(String alias)
определен в классе BaseBuilder
и используется для разрешения конкретного Java-класса по его псевдониму. Это означает, что мы можем регистрировать плагины MyBatis посредством псевдонимов.
Реализация метода resolveClass(String alias)
делегируется методу resolveAlias(String)
класса TypeAliasRegistry
:
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
// issue #748
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
if (TYPE_ALIASES.containsKey(key)) {
// загрузка из таблицы псевдонимов
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
// загрузка с помощью рефлексии
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Не удалось разрешить псевдоним типа '" + string + "'. Причина: " + e, e);
}
}
Реализация configuration.addInterceptor(interceptorInstance);
также не сводится к простому присваиванию, а включает добавление нового interceptor
в существующий список Configuration#interceptorChain#interceptors
.
Определение класса InterceptorChain
:
/**
* Цепочка обязанностей MyBatis-плагинов
*
* @author Clinton Begin
*/
public class InterceptorChain {
/**
* Все плагины
*/
private final List<Interceptor> interceptors = new ArrayList<>();
/**
* Вызов всех методов {@link Interceptor#plugin(Object)} для всех плагинов
*/
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
}
``````markdown
Таким образом, процесс разбора интерцептора начинается с разбора полного имени класса интерцептора (полное квалифицированное имя или псевдоним), после чего с помощью рефлексии получается объект-экземпляр интерцептора, а затем конфигурационные параметры `property`, указанные в конфигурационном файле, преобразуются в `Properties`, и метод `setProperties(Properties)` интерцептора вызывается для выполнения присваивания. В конце концов, интерцептор регистрируется в `Configuration#interceptorChain#interceptors`.
На этом разбор плагинов MyBatis завершен, и теперь следует разобрать объектный фабрикатор MyBatis.
### Разбор и конфигурация объектного фабрикатора MyBatis (objectFactory)
В MyBatis есть множество операций, которые используют рефлексию для создания объектов, например, при преобразовании возвращаемых результатов JDBC в конкретные сущности, объекты создаются с помощью рефлексии. Для таких операций создания объектов MyBatis предоставляет интерфейс API объектного фабрикатора, называемый `ObjectFactory`, его стандартная реализация — `DefaultObjectFactory`.
Класс `ObjectFactory` определен следующим образом:
/**
Фабрика для создания всех объектов в MyBatis
@author Clinton Begin */ public interface ObjectFactory {
/**
/**
/**
/**
}
````configuration -> objectFactoryиспользуется для конфигурации реализации объектного фабрикатора. ```Для
objectFactory` DTO определен следующим образом:
<!ELEMENT objectFactory (property*)>
<!ATTLIST objectFactory
type CDATA #REQUIRED
>
objectFactory
имеет обязательный атрибут type
, который используется для указания экземпляра ObjectFactory
. В этом атрибуте можно использовать псевдонимы. Внутри objectFactory
можно настроить один или несколько элементов property
, которые используются для установки параметров экземпляра ObjectFactory
.
// Настройка объекта создания фабрики
objectFactoryElement(root.evalNode("objectFactory"));
/**
* Разбор узла objectFactory
*
* @param context узел objectFactory
*/
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
// Получение атрибута type узла objectFactory
String type = context.getStringAttribute("type");
Properties properties = context.getChildrenAsProperties();
// Получение экземпляра фабрики
ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
factory.setProperties(properties);
// Настройка объекта создания фабрики
configuration.setObjectFactory(factory);
}
}
Эта реализация также проста: с помощью рефлексии получается экземпляр ObjectFactory
, затем свойства, сгенерированные из property
, устанавливаются в экземпляр, и в конце концов свойство Configuration#objectFactory
устанавливается.
Таким образом, разбор тега objectFactory
завершен.
ObjectWrapperFactory
— это фабрика объектов-оберток.В паттерне проектирования существует паттерн, называемый декоратором, который позволяет добавлять дополнительные функции к объекту без изменения его исходного поведения. В данном случае ObjectWrapperFactory
используется для создания обертки или декоратора для определенного объекта. ObjectWrapperFactory
интерфейс определен следующим образом:
```java
/**
* Объектный обертчик фабрика,主要用于包装返回的result对象,比如可以用来对返回的数据结果进行统一的操作,比如脱敏,加密等操作。
*
* @author Clinton Begin
* @see org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(ResultSetWrapper, ResultMap, String)
*/
public interface ObjectWrapperFactory {
/**
* Объект имеет декоратор
* @param object Заданный объект
*/
boolean hasWrapperFor(Object object);
/**
* Получить обертку для заданного объекта по метаданным объекта
* @param metaObject Метаданные объекта
* @param object Заданный объект
*/
ObjectWrapper getWrapperFor(MetaObject metaObject, Object object);
}
```}
В Mybatis для `objectWrapperFactory` DTO определен следующим образом:
```markdown
```xml
<!ELEMENT objectWrapperFactory EMPTY>
<!ATTLIST objectWrapperFactory
type CDATA #REQUIRED
>
`objectWrapperFactory` должен иметь атрибут `type`, который указывает на экземпляр `ObjectWrapperFactory`, который можно использовать с помощью псевдонима.
```markdown
```java
// Конфигурация объектного обертчика фабрики
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
```markdown
```java
/**
* Разбор узла objectWrapperFactory
*
* @param context objectWrapperFactory узел
*/
private void objectWrapperFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance();
configuration.setObjectWrapperFactory(factory);
}
}
Методы реализуются через рефлексию для получения экземпляра `ObjectWrapperFactory`, который затем присваивается свойству `Configuration#objectWrapperFactory`.
### Разбор и конфигурация фабрики рефлексии (узел reflectorFactory)
В предыдущем разделе мы упомянули `ReflectorFactory`, который является вспомогательной фабрикой для создания `Reflector` на основе класса. Он определяет три метода:
```markdown
```java
/**
* Интерфейс фабрики рефлексии
*/
public interface ReflectorFactory {
/**
* Включен ли кеш классов
*/
boolean isClassCacheEnabled();
/**
* Установка кеша классов
*/
void setClassCacheEnabled(boolean classCacheEnabled);
}
### Получение кэшированной информации для указанного класса
```java
/**
* Получение кэшированной информации для указанного класса
*/
Reflector findForClass(Class<?> type);
```Он создает объект `Reflector`, который используется для кэширования базовой информации о определенном классе, включая тип класса, имя читаемых и записываемых свойств, а также соответствующие методы `getter` и `setter`, конструкторы и т.д.
`DefaultReflectorFactory` является стандартной реализацией `ReflectorFactory` и предоставляет функциональность кэширования объектов `Reflector`. Кэширование основано на свойстве `reflectorMap` типа `ConcurrentMap<Class<?>, Reflector>`.
Для определения DTO `reflectorFactory` используется структура, аналогичная `objectWrapperFactory`:
```xml
<!ELEMENT reflectorFactory EMPTY>
<!ATTLIST reflectorFactory
type CDATA #REQUIRED
>
reflectorFactory
должен иметь атрибут type
, указывающий на экземпляр ReflectorFactory
, который может использовать псевдоним.
Методы его разбора аналогичны методам разбора objectWrapperFactory
:
// Настройка фабрики рефлектора
reflectorFactoryElement(root.evalNode("reflectorFactory"));
/**
* Разбор узла reflectorFactory
*
* @param context узел reflectorFactory
*/
private void reflectorFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ReflectorFactory factory = (ReflectorFactory) resolveClass(type).newInstance();
configuration.setReflectorFactory(factory);
}
}
Метод реализован через рефлексию для получения экземпляра ReflectorFactory
, который затем присваивается свойству Configuration#reflectorFactory
.
Разговор о том, как инициализировать глобальную конфигурацию MyBatis через settings
, приводит нас к ключевому объекту Configuration
.Объект Configuration
является одним из центральных объектов MyBatis, он определяет множество свойств и методов. В процессе анализа мы можем встретить некоторые свойства и методы, которые не понимаем, но это не важно, так как мы будем постепенно заполнять наши знания о Configuration
в процессе дальнейшего анализа.Вот пример свойств объекта Configuration
:
/**
* Класс конфигурации MyBatis
* Предоставляет все конфигурации MyBatis и контейнер метаданных для маппинговых файлов
*
* @author Clinton Begin
*/
public class Configuration {
* Информация об окружении, включая транзакции и источники данных
*/
protected Environment environment;
/**
* Разрешено ли использование {@link RowBounds} для пагинации в вложенных выражениях
*/
protected boolean safeRowBoundsEnabled;
/**
* Разрешено ли использование {@link ResultHandler} для пагинации в вложенных выражениях
*/
protected boolean safeResultHandlerEnabled = true;
/**
* Преобразование подчеркиваний в camelCase
*/
protected boolean mapUnderscoreToCamelCase;
/**
* Разрешено ли агрессивное ленивое загрузочное состояние
*/
protected boolean aggressiveLazyLoading;
/**
* Разрешено ли возврат нескольких результатов одним SQL-запросом
*/
protected boolean multipleResultSetsEnabled = true;
/**
* Разрешено ли использование JDBC для генерации ключей
*/
protected boolean useGeneratedKeys;
/**
* Использование меток столбцов вместо имен столбцов
*/
protected boolean useColumnLabel = true;
/**
* Включен ли кэш
*/
protected boolean cacheEnabled = true;
/**
* Установка значений для null-полей
*/
protected boolean callSettersOnNulls;
/**
* Использование реальных имен параметров, что может уменьшить количество кода, написанного с использованием {@link org.apache.ibatis.annotations.Param}
*/
protected boolean useActualParamName = true;
``````markdown
* Возврат пустого экземпляра при отсутствии данных
* По умолчанию, если результат запроса пустой, MyBatis возвращает null. Если включить эту опцию, будет возвращен пустой экземпляр.
*/
protected boolean returnInstanceForEmptyRow;
/**
* Префикс лога MyBatis
*/
protected String logPrefix;
/**
* Класс реализации лога, если не указан, будет автоматически определен
*/
protected Class<? extends Log> logImpl;
/**
* Виртуальная файловая система, предоставляющая простой API для доступа к системным файлам
*/
protected Class<? extends VFS> vfsImpl;
/**
* Срок жизни кэша
* MyBatis использует локальное кэширование для предотвращения циклических ссылок и ускорения повторных вложенных запросов.
* По умолчанию значение равно SESSION, что означает, что кэш будет использоваться для всех запросов в рамках одного сессионного объекта.
* Если значение равно STATEMENT, локальное кэширование будет использоваться только для одного SQL-запроса.
*/
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
/**
* Тип поля в базе данных
* Указывает JDBC-тип для параметра, если он не указан явно.
* Некоторые драйверы требуют указания типа столбца JDBC, обычно достаточно использовать общие типы, такие как NULL, VARCHAR или OTHER.
*/
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
/**
* Триггеры для ленивой загрузки
*/
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
/**
``` * Значение по умолчанию для таймаута {@link java.sql.Statement}, которое определяет количество секунд, в течение которых драйвер будет ожидать ответа от базы данных.
*/
protected Integer defaultStatementTimeout;
/**
* Значение по умолчанию для размера выборки данных
* Устанавливает значение по умолчанию для количества строк, которые будут загружены за один раз.
*/
protected Integer defaultFetchSize;
/**
* Тип поведения исполнителя, используемого при выполнении ожидаемых операций
* Включает:
* {@link ExecutorType#SIMPLE} создает новый подготовленный запрос {@link java.sql.PreparedStatement} для каждого исполненного запроса
* {@link ExecutorType#REUSE} переиспользует подготовленные запросы
* {@link ExecutorType#BATCH} переиспользует подготовленные запросы и выполняет операции в пакетном режиме
*/
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
/**
* Определение поведения автоматической отображаемости
* {@link AutoMappingBehavior#NONE} отключает автоматическую отображаемость
* {@link AutoMappingBehavior#PARTIAL} частичная автоматическая отображаемость, отображает только те результаты, для которых не определены вложенные отображаемости
* {@link AutoMappingBehavior#FULL} полная автоматическая отображаемость, автоматически отображает любые сложные результаты (независимо от того, вложены ли они)
*/
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
/**
* Поведение при обнаружении неизвестного поля при автоматической отображаемости
* {@link AutoMappingUnknownColumnBehavior#NONE} не выполняет никаких действий
* {@link AutoMappingUnknownColumnBehavior#WARNING} выводит предупреждение в лог
* {@link AutoMappingUnknownColumnBehavior#FAILING} прекращает автоматическую отображаемость и выбрасывает исключение
*/
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
/**
* Свойства конфигурации configuration->settings
*/
protected Properties variables = new Properties();
/**
* Конфигурация фабрики рефлексии для упрощения работы с свойствами и конструкторами
*/
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
/**
* Фабрика для создания конфигурационных объектов
*/
protected ObjectFactory objectFactory = new DefaultObjectFactory();
/**
* Фабрика для упаковки конфигурационных объектов,主要用于创建非原生对象
*/
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
/**
* Включено ли ленивое загрузка. Когда включено, все связанные объекты будут загружаться с отложением. В определенных связях можно перекрыть это состояние с помощью свойства fetchType.
*/
protected boolean lazyLoadingEnabled = false;
/**
* Фабрика прокси, указывает MyBatis на использование определенного инструмента для создания ленивых объектов
*/
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Используем встроенный Javassist вместо OGNL
/**
* Фабрика прокси, указывает MyBatis на использование определенного инструмента для создания ленивых объектов
*/ * Уникальный идентификатор типа базы данных, Mybatis использует этот идентификатор для выполнения различных запросов для разных производителей баз данных.
*/
protected String databaseId;
/**
* Фабрика конфигурации
* Configuration factory class.
* Используется для создания конфигурации для загрузки десериализованных непрочитанных свойств.
*
* @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
*/
protected Class<?> configurationFactory;
/**
* Регистратор DAO-операций
*/
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
/**
* Цепочка интерцепторов (Mybatis плагины)
*/
protected final InterceptorChain interceptorChain = new InterceptorChain();
/**
* Регистратор обработчиков типов
*/
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
/**
* Регистратор псевдонимов типов,主要用于执行SQL语句的输入和输出参数以及一些类的简写
*/
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
/**
* Регистратор драйверов языка
*/
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
/**
* Регистратор объявленных SQL-запросов
*/
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". Пожалуйста, проверьте " + savedValue.getResource() + " и " + targetValue.getResource());
/**
* Регистратор кэшированных запросов
*/
protected final Map<String, Cache> caches = new StrictMap<>("Коллекция кэшей");
/**
* Map-карта результатов
*/
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Коллекция карт результатов");
/**
* Карта параметров
*/
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Коллекция карт параметров");
/**
* Карта генераторов ключей
*/
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Коллекция генераторов ключей");
/**
* С集中已加载的资源集合,可以用来防止重复加载文件
*/
protected final Set<String> loadedResources = new HashSet<>();
/**
* Кодовые блоки
*/
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers"); /**
* Необработанные объявления
*/
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
/**
* Необработанные кэш-ссылки
*/
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
/**
* Необработанные результаты отображения
*/
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
/**
* Необработанные методы
*/
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
/*
* Отношения между кэш-ссылками и кэшами
* Кэш-ссылка => Кэш
*/
protected final Map<String, String> cacheRefMap = new HashMap<>();
public Configuration(Environment environment) {
this();
this.environment = environment;
} public Configuration() {
// Регистрация псевдонимов
} // Регистрация псевдонима JDBC
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
// Регистрация псевдонима для управления транзакциями
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
// Регистрация псевдонима JNDI
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
// Регистрация псевдонима для пула данных
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
// Регистрация псевдонима для непула данных
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
// Регистрация псевдонима для постоянного кэша
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
// Регистрация псевдонима для кэша FIFO
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
// Регистрация псевдонима для кэша LRU
typeAliasRegistry.registerAlias("LRU", LruCache.class);
// Регистрация псевдонима для мягкого кэша
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
// Регистрация псевдонима для слабого кэша
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
// Регистрация псевдонима для поставщика идентификатора базы данных
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
// Регистрация псевдонима для языкового драйвера XML
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
// Регистрация псевдонима для статического языкового драйвера
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
// Регистрация псевдонима для Sl4j логгера
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
// Регистрация псевдонима для Commons логгера
typeAliasRegistry. registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl. class);
// Регистрация псевдонима для log4j логгера
typeAliasRegistry. registerAlias("LOG4J", Log4jImpl. class);
// Регистрация псевдонима для log4j2 логгера
typeAliasRegistry. registerAlias("LOG4J2", Log4j2Impl. class);
// Регистрация псевдонима для JDK логгера
typeAliasRegistry. registerAlias("JDK_LOGGING", Jdk14LoggingImpl. class);
// Регистрация псевдонима для стандартного вывода логгера
typeAliasRegistry. registerAlias("STDOUT_LOGGING", StdOutImpl. class);
// Регистрация псевдонима для отсутствия логгера
typeAliasRegistry. registerAlias("NO_LOGGING", NoLoggingImpl. class);
// Регистрация псевдонима для CGLIB
typeAliasRegistry. registerAlias("CGLIB", CglibProxyFactory. class);
// Регистрация псевдонима для JAVASSIST
typeAliasRegistry. registerAlias("JAVASSIST", JavassistProxyFactory. class);```markdown
<! -- По умолчанию используется XML-драйвер -->
languageRegistry. setDefaultDriverClass(XMLLanguageDriver. class);
<! -- Поддерживается оригинальный язык -->
languageRegistry. register(RawLanguageDriver. class);
Эммм... Я еще раз все посмотрел и решил, что слишком много свойств, так что пока оставлю это на потом, позже буду изучать по одному.#### Инициализация глобальной конфигурации Mybatis через настройки
Вернемся к инициализации глобальной конфигурации через настройки.
```java
// Инициализация глобальной конфигурации через настройки
settingsElement(settings);
/**
* Замена системных настроек пользовательскими настройками
*
* @param props пользовательские настройки
* @see http://www.mybatis.org/mybatis-3/ru/configuration.html#settings
*/
private void settingsElement(Properties props) {
// Определение того, как Mybatis должен автоматически преобразовывать колонки JDBC в поля или свойства. Это соответствует перечислению AutoMappingBehavior, где
// NONE: отключает автоматическое отображение
// PARTIAL: частичное автоматическое отображение, только для тех результатов, которые не имеют определенных вложенных отображений
// FULL: полное автоматическое отображение, автоматически отображает любые сложные результаты (независимо от того, вложены ли они или нет)
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
// Определение того, как Mybatis должен действовать, когда автоматическое отображение не может обработать (распознать) колонки или поля. Это соответствует перечислению AutoMappingUnknownColumnBehavior, где
// NONE: ничего не делает
// WARNING: выводит предупреждение в лог
// FAILING: отображение завершается с ошибкой и выбрасывается исключение SqlSessionException
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
}
``````java
// Когда включен, вызов любого метода загружает все свойства объекта. В противном случае, каждый свойство загружается по мере необходимости (см. lazyLoadTriggerMethods).
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
// Разрешено ли возврат нескольких результатов из одного выражения (требуется совместимый драйвер).
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
// Использовать ли метки столбцов вместо имен столбцов.
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
``` // Разрешено ли JDBC автоматически генерировать первичные ключи (требуется совместимый драйвер).
// Если значение установлено в true, то этот параметр заставит использовать автоматическую генерацию первичных ключей, даже если некоторые драйверы не поддерживают это (например, Derby).
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false)); // Конфигурация по умолчанию для исполнителя. Соответствует enum ExecutorType.
// SIMPLE - обычный исполнитель;
// REUSE - исполнитель будет переиспользовать подготовленные выражения (prepared statements);
// BATCH - исполнитель будет переиспользовать выражения и выполнять операции в пакетном режиме.
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
// Установка времени ожидания по умолчанию. Определяет, сколько секунд драйвер будет ожидать ответа от базы данных.
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
// Установка значения подсказки для размера выборки (fetchSize) драйвера. Этот параметр может быть перекрыт только в настройках запроса.
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
// Включено ли автоматическое преобразование нижнего подчеркивания в стиль camelCase (camel case). Это означает, что из классического имени столбца A_COLUMN будет создано свойство aColumn в стиле camelCase.
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false)); // Разрешено ли использование RowBounds для пагинации в вложенных выражениях. Если разрешено, то значение должно быть false.
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false)); // MyBatis использует локальную кэшированную систему (Local Cache), чтобы предотвратить циклические ссылки (circular references) и ускорить повторные вложенные запросы.
// По умолчанию значение установлено на SESSION, в этом случае кэширование будет применяться ко всем запросам, выполненным в рамках одного сеанса.
// Если значение установлено на STATEMENT, локальная кэшированная система будет использоваться только для выполнения одного запроса, и данные не будут поделены между различными вызовами для одного и того же SqlSession.
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION"))); // В случае, когда JDBC-тип не указан для параметра, используется по умолчанию тип NULL, VARCHAR или OTHER, если параметр равен null.
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER"))); // Методы, которые могут быть использованы для триггерирования ленивой загрузки.
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals, clone, hashCode, toString"));
// Управление безопасностью обработчика результата. Если значение установлено в false, то использование обработчика результата не безопасно.
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
// Указание по умолчанию языка для динамического SQL.
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
// Указание по умолчанию TypeHandler для Enum.
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
// Управление вызовом методов setter для null значений в результатах.
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
// Управление использованием реальных имен параметров в сигнатуре метода.
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
// Управление возвратом пустого экземпляра вместо null, если все столбцы в строке пустые.
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false)); // Указание префикса, добавляемого MyBatis к имени лога.
configuration.setLogPrefix(props.getProperty("logPrefix"));```markdown
Укажите класс, который предоставляет экземпляр Configuration.
Этот возвращаемый экземпляр Configuration используется для загрузки ленивых свойств объектов, которые будут десериализованы.
Этот класс должен содержать статический метод getConfiguration() с сигнатурой static Configuration getConfiguration().
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
Необходимо отметить, что код внутри метода `settingsElement(Properties)` также довольно объемный. Давайте посмотрим на него по частям.
```##### Конфигурация автоматического отображения (настройка автоматического отображения в MyBatis)```java
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
AutoMappingBehavior — это стратегия, определяющая автоматическое отображение столбцов и полей в Mybatis. Он имеет три значения: NONE
, PARTIAL
, FULL
.
Когда мы определяем атрибуты collection
или association
в результатах запроса ResultMap, операция запроса не является простым SQL-запросом. Вместо этого результат представляет собой сложное множество, состоящее из одного и многих элементов. В этом случае это вложенный результат.
Например, мы определяем следующий маппер:
<!-- ClassInfo и Student имеют два атрибута: ID и name, но здесь мы не явно указываем их, используя автоматическое отображение Mybatis. -->
<!-- Определяем результат запроса для класса Student. -->
<resultMap id="Student" type="*.*.*.Student" >
</resultMap>
<!--
Определяем результат запроса для класса ClassInfo.
Отношение между классом и студентом: один ко многим.
-->
<resultMap id="ClassInfo" type="*.*.*.ClassInfo">
<collection property="student" resultMap="Student"/>
</resultMap>
```В этом случае результат запроса для ClassInfo включает коллекцию Student, где эта коллекция Student является вложенным результатом.Таким образом, только когда значение `Configuration#autoMappingBehavior` равно `AutoMappingBehavior#FULL`, автоматически будут присвоены значения ID и Name для объекта Student.
##### Конфигурация #autoMappingUnknownColumnBehavior (Настройка поведения Mybatis при автоматической маппинге столбцов, которые не могут быть идентифицированы)
```java
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
AutoMappingUnknownColumnBehavior
— это перечисление, определенное в Mybatis, которое описывает поведение при обнаружении столбцов или полей, которые не могут быть обработаны (идентифицированы) при автоматической маппинге. В этом перечислении определен метод doAction
, который реализует конкретные действия.
/**
* Выполняет действие при обнаружении неизвестного столбца (или неизвестного типа свойства) в автоматической маппинге.
*
* @param mappedStatement объявление отображения
* @param columnName имя столбца
* @param propertyName имя свойства
* @param propertyType тип свойства, если это свойство не null, то это указывает на отсутствие зарегистрированного TypeHandler.
*/
public abstract void doAction(MappedStatement mappedStatement, String columnName, String propertyName, Class<?> propertyType);
AutoMappingUnknownColumnBehavior
имеет три режима, из которых по умолчанию используется NONE
.
cacheEnabled — это переключатель MyBatis для включения всех кэшей, по умолчанию установленный в true
.
##### Конфигурация#proxyFactory(Модуль Mybatis для ленивой загрузки объектов)
configuration. setProxyFactory((ProxyFactory) createInstance(props. getProperty("proxyFactory")));
proxyFactory указывает на фабрику прокси-объектов, используемую Mybatis для создания объектов с поддержкой ленивой загрузки. По умолчанию используется JavassistProxyFactory.
configuration. setLazyLoadingEnabled(booleanValueOf(props. getProperty("lazyLoadingEnabled"), false));
lazyLoadingEnabled является глобальным переключателем для включения ленивой загрузки в Mybatis. При включении все связанные объекты будут загружаться лениво. Для конкретных связей можно переключить это состояние, установив свойство fetchType.
##### Конфигурация#aggressiveLazyLoading(Загрузка всех свойств объекта при вызове метода)
configuration. setAggressiveLazyLoading(booleanValueOf(props. getProperty("aggressiveLazyLoading"), false));
При включении любой вызов метода будет загружать все свойства объекта. В противном случае каждое свойство будет загружаться по мере необходимости. По умолчанию значение `false` (см. lazyLoadTriggerMethods).
configuration. setMultipleResultSetsEnabled(booleanValueOf(props. getProperty("multipleResultSetsEnabled"), true));
По умолчанию значение true
.
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
По умолчанию значение `true`.
##### Конфигурация#useGeneratedKeys(Возможность использования JDBC для генерации ключей)
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
Этот параметр используется для включения автоматической генерации ключей. По умолчанию значение `false`. Если параметр установлен в `true`, то ключи будут автоматически генерироваться, даже если некоторые драйверы не поддерживают это поведение.
##### Конфигурация#defaultExecutorType(Настройка по умолчанию для исполнителя SQL в Mybatis)
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
defaultExecutorType соответствует перечислению ExecutorType, которое определяет три типа исполнителей SQL в Mybatis:
- SIMPLE - обычный исполнитель, который создает новый подготовленный запрос для каждого выполнения;
- REUSE - исполнитель, который переиспользует подготовленные запросы;
- BATCH - исполнитель, который переиспользует запросы и выполняет их в виде пакета.
По умолчанию используется `SIMPLE`.
##### Конфигурация#defaultStatementTimeout(Установка времени ожидания запроса Mybatis)
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
Это значение определяет количество секунд, в течение которых Mybatis будет ожидать ответ от базы данных, по умолчанию — бесконечное ожидание.##### Конфигурация#defaultFetchSize(установка ограничения на количество строк в результирующем наборе)
configuration. setDefaultFetchSize(integerValueOf(props. getProperty("defaultFetchSize"), null));
Устанавливает предпочтительное значение для количества строк, которые драйвер должен получить из результирующего набора (fetchSize). Этот параметр может быть переопределен только в настройках запроса.
##### Конфигурация#mapUnderscoreToCamelCase(включение автоматического преобразования нижних подчеркиваний в стиль CamelCase)
configuration. setMapUnderscoreToCamelCase(booleanValueOf(props. getProperty("mapUnderscoreToCamelCase"), false));
При включении этого параметра Mybatis автоматически преобразует имена классов JDBC из стиля с нижними подчеркиваниями в стиль CamelCase для свойств Java. Значение по умолчанию — `false`.
##### Конфигурация#safeRowBoundsEnabled(разрешение использования RowBounds в вложенных запросах)
configuration. setSafeRowBoundsEnabled(booleanValueOf(props. getProperty("safeRowBoundsEnabled"), false));
Указывает, разрешено ли использование RowBounds в вложенных результирующих наборах. Значение по умолчанию — `false`, где `false` означает разрешение.
> RowBounds — это класс пагинации Mybatis, подробное использование будет описано позже.##### Конфигурация#localCacheScope(установка жизненного цикла локального кеша Mybatis)
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
`LocalCacheScope` — это перечисление, определяющее жизненный цикл локального кеша Mybatis. Оно имеет два значения, соответствующие различным жизненным циклам локального кеша:
- SESSION, в этом случае кеш будет хранить все запросы, выполненные в рамках одного сеанса.
- STATEMENT, локальный кеш будет использоваться только для выполнения запросов, и данные не будут разделяться между различными вызовами одного и того же `SqlSession`.
Значение по умолчанию — `SESSION`.
##### Конфигурация#jdbcTypeForNull(установка значения JDBC типа по умолчанию для параметров, равных NULL)
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
Это значение определяет тип JDBC, используемый по умолчанию для параметров, равных NULL, когда тип JDBC для параметра не указан. Обычно это один из типов NULL, VARCHAR или OTHER. Значение по умолчанию — `OTHER`.
##### Конфигурация#lazyLoadTriggerMethods(установка методов, вызывающих ленивую загрузку в Mybatis)
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
Этот параметр упоминается в `aggressiveLazyLoading`, и оба параметра обычно используются вместе. Он определяет методы, вызывающие ленивую загрузку, и указывает, какие методы объектов вызывают ленивую загрузку. По умолчанию значение равно: `equals,clone,hashCode,toString`.##### Конфигурация#safeResultHandlerEnabled(разрешение использования ResultHandler для пагинации в вложенных выражениях)
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
Устанавливает, разрешено ли MyBatis использование ResultHandler для пагинации в вложенных выражениях. Внимание!! Если разрешено, устанавливайте значение в `false`. По умолчанию значение равно `false`.
##### Конфигурация#languageRegistry(регистратор языковых драйверов для MyBatis скриптов)
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
/**
* Устанавливает по умолчанию язык скриптов для MyBatis
*
* @param driver язык скриптов
*/
public void setDefaultScriptingLanguage(Class<? extends LanguageDriver> driver) {
if (driver == null) {
driver = XMLLanguageDriver.class;
}
// Регистрирует язык скриптов по умолчанию в регистраторе языковых драйверов
getLanguageRegistry().setDefaultDriverClass(driver);
}
public LanguageDriverRegistry getLanguageRegistry() { return languageRegistry; }
/** * Устанавливает тип по умолчанию для языка скриптов * * @param defaultDriverClass тип по умолчанию для языка скриптов */ public void setDefaultDriverClass(Class<? extends LanguageDriver> defaultDriverClass) { // Регистрирует тип по умолчанию для языка скриптов register(defaultDriverClass); this.defaultDriverClass = defaultDriverClass; }
Устанавливает по умолчанию язык скриптов для MyBatis. По умолчанию используется `XMLLanguageDriver` SQL-драйвер.В интерфейсе `LanguageDriver` определены три метода:
```markdown
## Создание обработчика параметров
```java
/**
* Создает {@link ParameterHandler}, который передает фактические параметры JDBC-запросу.
*
* @param mappedStatement Заявление, которое выполняется
* @param parameterObject Объект входных параметров
* @param boundSql Сгенерированное SQL
* @return Обработчик параметров
* @author Frank D. Martinez [mnesarco]
* @see DefaultParameterHandler
*/
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
/**
* Создает {@link SqlSource}, который сохраняет SQL-запросы, прочитанные из XML-файла маппера.
* SQL-запросы из XML-файла читаются при запуске.
*
* @param configuration Конфигурация Mybatis
* @param script XNode, прочитанный из XML-файла
* @param parameterType Тип входных параметров
* @return Источник SQL
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
/**
* Создает {@link SqlSource}, который сохраняет SQL-запросы, прочитанные из XML-файла маппера.
* SQL-запросы из класса читаются при запуске.
*
* @param configuration Конфигурация Mybatis
* @param script Содержимое аннотации
* @param parameterType Тип входных параметров
* @return Источник SQL
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
```Кроме драйвера XMLLanguageDriver
в Mybatis, типичным примером использования является драйвер `MybatisXMLLanguageDriver` в `Mybatis-plus`, который переопределяет метод `createParameterHandler` драйвера `XMLLanguageDriver`, чтобы использовать пользовательский `MybatisDefaultParameterHandler`.`LanguageDriverRegistry` — это регистр обработчиков SQL-запросов Mybatis. Он поддерживает коллекцию всех типов драйверов, которые могут обрабатывать SQL-запросы в Mybatis. Внутри него хранится коллекция `LANGUAGE_DRIVER_MAP`, которая содержит типы обработчиков SQL-запросов и их экземпляры.
/**
* Регистр обработчиков SQL-запросов Mybatis
*
* @author Frank D. Martinez [mnesarco]
*/
public class LanguageDriverRegistry {
/**
* Регистр обработчиков SQL-запросов
* Тип обработчика SQL-запросов => Экземпляр обработчика SQL-запросов
*/
private final Map<Class<? extends LanguageDriver>, LanguageDriver> LANGUAGE_DRIVER_MAP = new HashMap<>();
/**
* По умолчанию используется обработчик SQL-запросов
*/
private Class<? extends LanguageDriver> defaultDriverClass;
}
/**
* Регистрация нового обработчика языка Mybatis
*
* @param cls тип обработчика языка
*/
public void register(Class<? extends LanguageDriver> cls) {
if (cls == null) {
throw new IllegalArgumentException("null is not a valid Language Driver");
}
if (!LANGUAGE_DRIVER_MAP.containsKey(cls)) {
try {
// Регистрация
LANGUAGE_DRIVER_MAP.put(cls, cls.newInstance());
} catch (Exception ex) {
throw new ScriptingException("Failed to load language driver for " + cls.getName(), ex);
}
}
}
/**
* Регистрация нового обработчика языка Mybatis
*
* @param instance обработчик языка
*/
public void register(LanguageDriver instance) {
if (instance == null) {
throw new IllegalArgumentException("null is not a valid Language Driver");
}
// Получение типа Class
Class<? extends LanguageDriver> cls = instance.getClass();
if (!LANGUAGE_DRIVER_MAP.containsKey(cls)) {
// Регистрация
LANGUAGE_DRIVER_MAP.put(cls, instance);
}
}```
Метод configuration#setDefaultScriptingLanguage(Class)
вызывает метод setDefaultDriverClass
из класса LanguageDriverRegistry
для установки глобального по умолчанию обработчика языка SQL. Из configuration#setDefaultScriptingLanguage(Class)
также можно увидеть, что по умолчанию Mybatis использует XMLLanguageDriver
в качестве обработчика языка SQL.
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")))
/** * Устанавливает по умолчанию TypeHandler для обработки типов перечислений. По умолчанию используется EnumTypeHandler. * * @param typeHandler TypeHandler для обработки типов перечислений * @since 3.4.5 */ public void setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler) { if (typeHandler != null) { // Получаем TypeHandlerRegistry и устанавливаем по умолчанию TypeHandler для обработки типов перечислений getTypeHandlerRegistry().setDefaultEnumTypeHandler(typeHandler); } }
Получаем экземпляр TypeHandlerRegistry из Configuration.
/**
* Получает TypeHandlerRegistry
*/
public TypeHandlerRegistry getTypeHandlerRegistry() {
return typeHandlerRegistry;
}
Устанавливаем по умолчанию TypeHandler для обработки типов перечислений.
/** * Устанавливает по умолчанию TypeHandler для обработки типов перечислений. По умолчанию используется EnumTypeHandler. * * @param typeHandler TypeHandler для обработки типов перечислений * @since 3.4.5 */ public void setDefaultEnumTypeHandler(Class<? extends TypeHandler> typeHandler) { this.defaultEnumTypeHandler = typeHandler; }
`TypeHandlerRegistry` является регистром TypeHandler для Mybatis, который отвечает за поддержание TypeHandler для преобразования типов Java и JDBC.`TypeHandlerRegistry` использует свойство `defaultEnumTypeHandler` для записи по умолчанию TypeHandler для перечислений.
Параметр `defaultEnumTypeHandler` используется для установки значения по умолчанию для `TypeHandlerRegistry#defaultEnumTypeHandler`, по умолчанию это `EnumTypeHandler`.
##### Configuration#callSettersOnNulls(указывает, следует ли вызывать методы setter (или put для map) для свойств объекта, значение которых равно null в результирующем наборе)
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
Устанавливает, следует ли вызывать методы setter для свойств объекта, значение которых равно null в результирующем наборе. Значение по умолчанию равно `false`.
##### Конфигурация#returnInstanceForEmptyRow(При пустом запросе, создавать ли пустой экземпляр)
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
Устанавливает, создавать ли пустой экземпляр при пустом результирующем наборе MyBatis. Значение по умолчанию — `false`, то есть создание не производится.
> Важно отметить, что при включении этой функции, она также применяется к вложенным результатам (collection и association).
##### Конфигурация#logPrefix(Устанавливает общую префиксную метку для логов MyBatis)
configuration.setLogPrefix(props.getProperty("logPrefix"));
Устанавливает общую префиксную метку для всех логов MyBatis.##### Конфигурация#configurationFactory(Указывает фабрику конфигурации)
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
Указывает класс, который предоставляет экземпляр `Configuration`. Этот экземпляр `Configuration` используется для загрузки лениво-загружаемых свойств объектов, которые были десериализованы.> Класс, указанный в `configurationFactory`, должен иметь статический метод `static Configuration getConfiguration()`.
Хорошо, процесс конфигурации `Configuration` на основе элемента `settings` завершен. Этот раздел довольно сухой и сложный, поэтому достаточно иметь общее представление о различных полях.
### Разбор элемента environments, конфигурация нескольких окружений MyBatis
После завершения сухого процесса конфигурации `Configuration` на основе элемента `settings`, переходим к разбору элемента `environments`, чтобы настроить несколько окружений в MyBatis.
MyBatis по умолчанию поддерживает конфигурацию нескольких окружений. В MyBatis существует объект `Environment`, который имеет три простых параметра:
/**
В этом контексте `id` является уникальным идентификатором текущего окружения, что является семантическим свойством.
`transactionFactory` представляет собой объект `TransactionFactory`, который является фабрикой для создания объектов `Transaction`. Объект `Transaction` упаковывает `Connection` JDBC и управляет жизненным циклом соединения с базой данных, включая его создание, подтверждение/откат и закрытие. `dataSource` свойство соответствует объекту `DataSource`, который указывает на JDBC источник данных.DTD определение для `environments` в MyBatis выглядит следующим образом:
В `environments` необходимо указать значение атрибута `default`, которое является идентификатором по умолчанию для среды и используется для указания используемой по умолчанию среды. В `environments` также допускается наличие одного или более `environment` дочерних элементов.
Элемент `environment` имеет обязательный атрибут `id`, который является уникальным маркером конфигурации текущей среды, а также содержит обязательные элементы `transactionManager` и `dataSource`.
#### transactionManager
Элемент `transactionManager` используется для конфигурации текущего менеджера транзакций, его DTD определение выглядит следующим образом:
Элемент `transactionManager` имеет обязательный атрибут `type`, который указывает тип используемого менеджера транзакций. В MyBatis по умолчанию предоставляются два типа менеджеров транзакций: `JDBC` и `MANAGED`.
Эти два кратких имени опираются на механизм псевдонимов типов в MyBatis, они регистрируются в конструкторе без параметров класса `Configuration`:
public Configuration() {
// Регистрация псевдонимов
}
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
// Регистрация псевдонима менеджера транзакций
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
...
}
Где JDBC
соответствует классу JdbcTransactionFactory
, который создает объект JdbcTransaction
, использующий стандартные настройки подтверждения и отмены транзакций JDBC, зависящие от соединения, полученное из источника данных.А MANAGED
соответствует классу ManagedTransactionFactory
, который создает объект ManagedTransaction
, являющийся одним из реализаций интерфейса Transaction
, который игнорирует все запросы подтверждения и отмены транзакций, а также по умолчанию имеет возможность закрытия соединения, хотя это можно настроить так, чтобы эта возможность была отключена.
Также в элементе transactionManager
можно настроить несколько элементов property
, используемых для параметров пользовательского менеджера транзакций.
Элемент dataSource
используется для конфигурации JDBC источника данных, его DTD определение выглядит следующим образом:
<!ELEMENT dataSource (property*)>
<!ATTLIST dataSource
type CDATA #REQUIRED
>
dataSource
тег также имеет обязательное свойство type
, которое используется для указания конкретного экземпляра JDBC data source. В MyBatis по умолчанию предоставляются три типа источников данных: UNPOOLED
, POOLED
и JNDI
.Аналогично, эти три кратких имени также являются псевдонимами типов Mybatis, которые регистрируются в конструкторе без параметров объекта Configuration
:
public Configuration() {
// регистрация псевдонимов
...
// регистрация псевдонима JNDI
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
// регистрация псевдонима пула данных
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
// регистрация псевдонима непула данных
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
...
}
В частности, JNDI
соответствует JndiDataSourceFactory
, который используется для загрузки доступного DataSource
с помощью JNDI.POOLED
соответствует PooledDataSourceFactory
, который используется для получения PooledDataSource
. PooledDataSource
представляет собой простой, синхронизированный, потокобезопасный пул подключений к базе данных, который использует повторное использование объектов JDBC Connection
, чтобы избежать времени инициализации и аутентификации при создании новых подключений, что повышает способность приложения к параллельному доступу к базе данных.
UNPOOLED
соответствует UnpooledDataSourceFactory
, который используется для получения UnpooledDataSource
. UnpooledDataSource
представляет собой простой источник данных, который для каждого запроса на получение подключения открывает новое подключение.Под тегом dataSource
допускается наличие нескольких тегов property
, которые будут преобразованы в Properties
для инициализации и конфигурации соответствующего DataSource
.
Понимая функции каждого элемента, мы продолжаем анализ кода для разбора environments
. Вызов парсера (XmlConfigBuilder
):
// Загрузка конфигураций для нескольких окружений, поиск текущего окружения (по умолчанию - default) соответствующего менеджера транзакций и источника данных
environmentsElement(root.evalNode("environments"));
Парсинг и построение окружения MyBatis.
/**
* Парсинг узла environments
*
* @param context узел environments
*/
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
// Конфигурация по умолчанию
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
// Получение уникального идентификатора окружения
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
// Конфигурация по умолчанию источника данных
// Создание фабрики транзакций
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// Создание фабрики источника данных
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
// Создание источника данных
DataSource dataSource = dsFactory.getDataSource();
Чтобы создать контейнер среды с уникальным идентификатором среды, фабрикой транзакций и источником данных, используется следующий код:```java Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
Процесс анализа тега `environments` не является сложным. Он начинается с получения уникального идентификатора указанной пользователем среды по свойству `default` тега `environments`.
Затем происходит перебор всех тегов `environment` внутри тега `environments`. Уникальный идентификатор конфигурируемой среды получается по свойству `id` тега `environment`.
Если текущий идентификатор совпадает с указанным пользователем идентификатором по умолчанию, то обрабатывается этот тег `environment`. В противном случае обработка пропускается.
```java
private boolean isSpecifiedEnvironment(String id) {
if (environment == null) {
throw new BuilderException("No environment specified.");
} else if (id == null) {
throw new BuilderException("Environment requires an id attribute.");
} else if (environment.equals(id)) {
return true;
}
return false;
}
```При обработке тега `environment` используются конфигурации элементов `transactionManager` и `dataSource` для получения объектов `TransactionFactory` и `DataSourceFactory`. Затем с помощью `DataSourceFactory` получается экземпляр объекта `DataSource`. В конце используется конструктор объекта `Environment`, чтобы создать объект `Environment`, который затем синхронизируется с конфигурационным классом MyBatis `Configuration`.
```Процесс разбора `transactionManager`:
// Создание фабрики транзакций
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
/**
* Настройка механизма управления транзакциями на основе элемента environments>environment>transactionManager
*
* @param context содержимое элемента environments>environment>transactionManager
*/
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
// Получение типа транзакции
String type = context.getStringAttribute("type");
// Получение конфигурации параметров транзакции
Properties props = context.getChildrenAsProperties();
// Создание фабрики транзакций
TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
// Установка пользовательских параметров
factory.setProperties(props);
return factory;
}
throw new BuilderException("Настройка окружения требует фабрики транзакций.");
}
Процесс разбора `transactionManager` довольно прост. Сначала получается атрибут `type` элемента `transactionManager`, который представляет собой псевдоним управляющего транзакциями.Затем с помощью метода `BaseBuilder#resolveClass()` (этот метод был рассмотрен ранее) из таблицы псевдонимов MyBatis находит конкретный тип фабрики транзакций. С помощью рефлексии создается экземпляр `TransactionFactory`, а затем методом `TransactionFactory#setProperties(Properties)` устанавливаются пользовательские параметры. Таким образом, элемент `transactionManager` успешно разобран. Процесс парсинга элемента `dataSource`:
```java
// Создаем фабрику данных
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
/**
* Парсинг элемента dataSource
*/
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
// Получаем алиас DataSourceFactory, указанный атрибутом type элемента dataSource.
String type = context.getStringAttribute("type");
// Получаем коллекцию параметров конфигурации, определенных пользователем.
Properties props = context.getChildrenAsProperties();
// Парсим алиас и получаем рефлексию для конкретного типа DataSourceFactory.
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
// Устанавливаем параметры
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
Процесс парсинга элемента dataSource
очень похож на процесс парсинга элемента transactionManager
.Сначала по атрибуту type
получаем псевдоним для DataSourceFactory
, затем получаем коллекцию параметров конфигурации, определённых пользователем, далее парсим псевдоним и получаем рефлексию для конкретного типа DataSourceFactory
, и в конце устанавливаем параметры конфигурации в DataSourceFactory
.После завершения парсинга и инициализации transactionManager
и dataSource
, MyBatis использует DataSourceFactory
для получения конкретного экземпляра DataSource
. В этот момент все необходимые элементы для конфигурации объекта Environment
уже получены. Затем с помощью конструктора объекта Environment
создается экземпляр Environment
, который синхронизируется с атрибутом environment
класса конфигурации MyBatis Configuration
.
Таким образом, парсинг элемента environments
завершен.
databaseIdProvider
, конфигурация генератора уникального идентификатора базы данныхMyBatis предоставляет возможность выполнения различных SQL-запросов в зависимости от типа используемой базы данных. Это достигается за счет использования атрибута databaseId
в SQL-запросах. После определения типа текущей базы данных, MyBatis загружает все SQL-запросы, которые не имеют атрибута databaseId
, а также те, которые соответствуют текущему типу базы данных. Если SQL-запросы имеют как атрибут databaseId
, так и не имеют его, то будут использованы запросы с атрибутом databaseId
. Что касается определения текущего типа базы данных и преобразования его в databaseId
, эта функция реализуется интерфейсом DatabaseIdProvider
, который определяет метод getDatabaseId(DataSource)
. Этот метод позволяет получить databaseId
для указанного источника данных.Элемент databaseIdProvider
используется для указания конкретной реализации класса DatabaseIdProvider
, используемой Mybatis.
Его DTD определен следующим образом:
<!ELEMENT databaseIdProvider (property*)>
<!ATTLIST databaseIdProvider
type CDATA #REQUIRED
>
Элемент databaseIdProvider
имеет обязательный атрибут type
, который указывает реализацию класса DatabaseIdProvider
, используемую Mybatis для генерации databaseId
. Этот параметр может использовать псевдонимы Mybatis.
В настоящее время Mybatis регистрирует только один псевдоним DB_VENDOR
в конфигурации, который указывает на экземпляр класса VendorDatabaseIdProvider
.
public Configuration() {
...
// Регистрация поставщика идентификатора базы данных
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
...
}
VendorDatabaseIdProvider
использует DatabaseMetaData#getDatabaseProductName
для определения databaseId
.
Когда пользователь использует VendorDatabaseIdProvider
как поставщика уникального идентификатора базы данных, ему необходимо предоставить соответствующие отношения между типом базы данных и databaseId
.Тип базы данных используется для проверки, содержит ли имя продукта метаданные указанного источника данных значение, и возвращает databaseId
в соответствии с определением пользователя. Пользователь определяет связь между типом базы данных и databaseId
с помощью элемента databaseIdProvider
и его подэлемента property
.
Например:
<databaseIdProvider type="DB_VENDOR">
<property name="Apache Derby" value="derby"/>
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>
Далее переходим к коду для парсинга элемента databaseIdProvider
:
/**
* Парсинг элемента databaseIdProvider
*
* @param context элемент databaseIdProvider
*/
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// ужасный патч для обратной совместимости
if ("VENDOR".equals(type)) {
type = "DB_VENDOR";
}
// получаем конфигурацию пользовательских типов базы данных и databaseId
Properties properties = context.getChildrenAsProperties();
// получаем экземпляр DatabaseIdProvider
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
// настраиваем соответствие типов базы данных и databaseId
databaseIdProvider.setProperties(properties);
}
// получаем контейнер Environment
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
// получаем текущий databaseId
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
}
}
``` // синхронизируем значение configuration#databaseId
configuration.setDatabaseId(databaseId);
}
}
Парсинг элемента databaseIdProvider
довольно прост. Сначала получаем имя экземпляра DatabaseIdProvider
, указанное атрибутом type
элемента databaseIdProvider
. Затем обеспечиваем совместимость псевдонима VENDOR
с DB_VENDOR
. Затем используем метод BaseBuilder#resolveClass(String)
для получения типа экземпляра DatabaseIdProvider
, а затем используем рефлексию для получения соответствующего экземпляра и инициализации соответствия типов базы данных и databaseId
. После получения объекта Environment
(из Configuration#environment
) для текущей среды MyBatis, используем метод getDatabaseId(DataSource)
из полученного экземпляра DatabaseIdProvider
, чтобы получить значение databaseId
для текущей среды, а затем присваиваем это значение Configuration#databaseId
.На этом парсинг элемента databaseIdProvider
завершен.
typeHandlers
Mybatis, конфигурация типовых обработчиков преобразования типовВ предыдущих статьях мы упоминали, что TypeHandlerRegistry
поддерживает регистр типовых обработчиков преобразования типов Mybatis.
Типовые обработчики преобразования типов (TypeHandler
) представляют собой интерфейс, определяющий стратегию преобразования типов Java и JDBC. Они должны предоставлять следующие четыре метода:
PreparedStatement
по индексу параметра и типу JDBC с помощью метода setParameter(PreparedStatement, int, T, JdbcType)
.ResultSet
по имени и преобразование его в соответствующий тип Java с помощью метода getResult(ResultSet, String)
.ResultSet
по индексу и преобразование его в соответствующий тип Java с помощью метода getResult(ResultSet, int)
.CallableStatement
по индексу и преобразование его в соответствующий тип Java с помощью метода getResult(CallableStatement, int)
.В TypeHandlerRegistry
по умолчанию регистрируются некоторые часто используемые типовые обработчики преобразования типов. Конкретные коды регистрации можно найти в конструкторе без параметров TypeHandlerRegistry
.
В течение всего жизненного цикла Mybatis, конструктор без параметров TypeHandlerRegistry
вызывается в методе XMLConfigBuilder#XMLConfigBuilder(XPathParser, String, Properties)
через super(new Configuration())
.В Configuration
есть переменная typeAliasRegistry
, отмеченная как final
, которая инициализируется в процессе инициализации Configuration
через вызов конструктора без параметров typeAliasRegistry
.
/**
* Регистр типовых псевдонимов, используется для упрощения входных и выходных параметров SQL-запросов и некоторых классов
*/
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
Хотя Mybatis по умолчанию регистрирует множество часто используемых TypeHandler
, при работе с сложными пользовательскими объектами часто требуется предоставить свои собственные типовые обработчики преобразования типов, например, для пользовательских обработчиков преобразования типов перечислений. В этом случае нам нужно зарегистрировать наш обработчик преобразования типов в Mybatis, и мы можем использовать typeHandlers
для конфигурации.
В DTD Mybatis для typeHandlers
определено следующим образом:
<!ELEMENT typeHandlers (typeHandler*, package*)>
Под элементом typeHandlers
могут располагаться ноль или более элементов typeHandler
или package
.
Каждый элемент typeHandler
имеет три атрибута: javaType
, jdbcType
и handler
. Атрибуты javaType
и jdbcType
являются необязательными и представляют собой типы Java и JDBC соответственно. Атрибут handler
является обязательным и указывает на тип обработчика преобразования типов. Каждый элемент typeHandler
соответствует конфигурации одного экземпляра TypeHandler
.Эти три параметра могут использовать механизм псевдонимов MyBatis, так как их разрешение происходит с помощью метода BaseBuilder#resolveClass(String)
.Элемент package
имеет один обязательный атрибут name
, который представляет собой базовый пакет, который пользователь хочет зарегистрировать. MyBatis будет рекурсивно обрабатывать все доступные экземпляры TypeHandler
в базовом пакете и его подпакетах. Анализ позиции входа для typeHandlers
находится в методе XmlConfigBuilder#parseConfiguration(XNode)
:
// Регистрация обработчиков типов
typeHandlerElement(root.evalNode("typeHandlers"));
Этот метод делегирует задачу парсинга и регистрации typeHandlers
методу typeHandlerElement(XNode)
:
// Парсинг элемента typeHandlers
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// Регистрация всего пакета
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
// Регистрация отдельных элементов
// Получение имени Java-типа
String javaTypeName = child.getStringAttribute("javaType");
// Получение имени JDBC-типа
String jdbcTypeName = child.getStringAttribute("jdbcType");
// Получение имени типа обработчика
String handlerTypeName = child.getStringAttribute("handler");
// Парсинг Java-типа
Class<?> javaTypeClass = resolveClass(javaTypeName);
// Парсинг JDBC-типа
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
// Парсинг типа обработчика
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
Для регистрации обработчиков типов Mybatis разделяет обработку на две категории: парсинг элемента package
и парсинг элемента typeHandler
.
package
Элемент package
имеет один обязательный атрибут name
, который указывает базовый пакет, содержащий обработчики типов. Mybatis передает это значение в метод TypeHandlerRegistry.register(String)
для выполнения регистрации всех классов в этом пакете.
В методе TypeHandlerRegistry.register(String)
сначала используется ResolverUtil
(инструмент для поиска всех классов, соответствующих определенным условиям) для поиска всех классов, реализующих интерфейс TypeHandler
, в указанном пакете. Затем исключаются анонимные классы, интерфейсы и абстрактные классы, и оставшиеся классы передаются методу register(Class)
для дальнейшей обработки.
// Регистрация всех Java-классов в указанном пакете
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// Поиск всех подклассов TypeHandler в указанном пакете
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
// Игнорирование вложенных классов, интерфейсов (включая package-info.java) и абстрактных классов
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
// Регистрация обработчика типов
register(type);
}
}
}
```##### Парсинг элемента `typeHandler`
Элемент `typeHandler` регистрируется в зависимости от наличия атрибута `jdbcType`. Если атрибут `jdbcType` отсутствует, регистрируется только класс обработчика типов. Если атрибут `jdbcType` присутствует, регистрируется комбинация класса обработчика типов и типа JDBC.
```java
// Регистрация обработчиков типов
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
MappedTypes
В методе register(Class)
сначала извлекается аннотация MappedTypes
(аннотация, используемая для указания Java-типов, которые обрабатывает обработчик типов).
```Если на процессоре типов присутствует аннотация MappedTypes
, то регистрация будет продолжена с помощью метода `register(Class>, Class>)`. В противном случае, регистрация будет выполнена методом `register(TypeHandler)`.
Здесь код делит выполнение на ветки, и цепочка вызовов довольно длинная, что затрудняет запоминание, но это не столь важно, так как обе ветки в конечном итоге сойдутся в одном методе. Мы сначала разберем эти две ветки по порядку, сделаем пометку и запомним, что нам нужно вернуться к этому методу позже.
Сначала мы обрабатываем первую ветку, которая имеет аннотацию MappedTypes
.
```Метод register(Class<?>, Class<?>)
вызывает метод `getInstance(Class>, Class>)` для получения экземпляра `TypeHandler`, после чего делегирует выполнение методу `register(Class, TypeHandler extends T>)` для завершения регистрации. Метод `getInstance(Class>, Class<?>)` предназначен для создания экземпляра типа конвертера на основе заданного Java типа и типа обработчика конвертации:
/**
* Получает обработчик конвертации на основе заданного Java типа и типа обработчика конвертации
*
* @param javaTypeClass Java тип
* @param typeHandlerClass Тип обработчика конвертации
* @param <T> Java тип
*/
@SuppressWarnings("unchecked")
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
if (javaTypeClass != null) {
try {
// Получает конструктор обработчика конвертации с параметром типа Class
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
// Создает экземпляр обработчика конвертации через конструктор
return (TypeHandler<T>) c.newInstance(javaTypeClass);
} catch (NoSuchMethodException ignored) {
// ignored
} catch (Exception e) {
throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
}
}
try {
// Получает конструктор обработчика конвертации без параметров
Constructor<?> c = typeHandlerClass.getConstructor();
// Создает экземпляр обработчика конвертации через конструктор
return (TypeHandler<T>) c.newInstance();
} catch (Exception e) {
throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
}
}
Метод register(Class<T>, TypeHandler<?>
extends T>)имеет интересную функциональность, которая заключается в том, чтобы преобразовать
Classв тип
Typeи затем передать управление методу
register(Type javaType, TypeHandler extends T> typeHandler)` для завершения регистрации. Здесь метод `register(Type javaType, TypeHandler extends T> typeHandler)` является тем конечным методом, о котором мы говорили ранее, когда ставили метки. Вместо того чтобы объяснять этот метод, мы продолжим обрабатывать вторую ветвь, которую мы рассматривали ранее: обработку случаев, когда отсутствует аннотация `MappedTypes`.Когда на указанном классе `TypeHandler` отсутствует аннотация `MappedTypes`, MyBatis сначала вызывает метод `getInstance(Class>, Class<?>), чтобы получить экземпляр
TypeHandler, а затем делегирует метод
register(TypeHandler)` для обработки.
В методе register(TypeHandler<T>)
MyBatis сначала пытается получить аннотацию MappedTypes
. Здесь важно отличать этот метод от register(Class<?>)
. В данном случае аннотация MappedTypes
снова получается, и я проследил за вызовом этого метода, который используется только в методе register(Class<?>)
за исключением юнит-тестов. Однако в методе register(Class<?>)
этот вызов происходит только в случае отсутствия аннотации MappedTypes
. Поэтому следующий фрагмент кода можно считать излишним:
boolean mappedTypeFound = false;
// Получаем аннотацию MappedTypes
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> handledType : mappedTypes.value()) {
// Регистрируем типы обработчиков на основе аннотации
register(handledType, typeHandler);
mappedTypeFound = true;
}
}
Почему это можно считать излишним, но не абсолютно излишним? Потому что этот код оставляет возможность для прямого использования объекта TypeHandler
для регистрации типов.
Возвращаясь к основной теме, метод register(TypeHandler<T>)
предлагает три сценария регистрации для TypeHandler
:
Если TypeHandler
имеет аннотацию MappedTypes
, то на основе значений аннотации MappedTypes
вызывается метод register(Type, TypeHandler<? extends T>)
для выполнения регистрации.- Если отсутствует аннотация MapperTypes
, но класс TypeHandler
наследует абстрактный класс TypeReference
, то получается значение типа TypeHandler
, и затем вызывается метод register(Type, TypeHandler<? extends T>)
для выполнения регистрации.
Если ни одно из вышеперечисленных условий не выполняется, то вызывается метод register(Class<T>, TypeHandler<? extends T>)
, который преобразует null в тип Type
, и затем вызывается метод register(Type, TypeHandler<? extends T>)
для выполнения регистрации.
Перед тем как рассмотреть метод register(Type, TypeHandler<? extends T>)
, давайте сначала познакомимся с абстрактным классом TypeReference
. TypeReference
— это абстрактный класс, который используется для получения ссылок на параметризованные типы классов, например:
class TypeReferenceTest extends TypeReference<Integer> {
}
TypeReferenceTest
наследует абстрактный класс TypeReference
и указывает параметризованный тип как Integer
. При вызове метода getRawType()
у экземпляра TypeReferenceTest
мы получаем Integer.class
.
MyBatis определяет абстрактный класс BaseTypeHandler
, который наследует TypeReference
и реализует интерфейс TypeHandler
. Этот класс является базовым классом для встроенных типов обработчиков в MyBatis.Определение BaseTypeHandler
выглядит следующим образом:
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T>
Кроме того, BaseTypeHandler
реализует методы по умолчанию, определенные в интерфейсе TypeHandler
, добавляет базовую проверку данных и предоставляет новые абстрактные методы для реализации дочерними классами. Он также добавляет ссылку на конфигурационный класс Configuration
MyBatis и предоставляет соответствующие методы установки.Переходим к методу register(Type, TypeHandler<? extends T>)
класса TypeHandlerRegistry
. Мы упоминали ранее, что есть один метод, который является альтернативным способом выполнения той же задачи, и это именно этот метод. Независимо от сценария, если мы можем вызвать этот метод, это означает, что у нас есть два параметра: тип Java (Type
) и экземпляр обработчика типа (TypeHandler
). Хотя первый параметр типа Java может быть null
.
После входа в этот метод, сначала ищется аннотация MappedJdbcTypes
на экземпляре TypeHandler
.
Аннотация
MappedJdbcTypes
используется для указания JdbcType, который обрабатывается TypeHandler. Она имеет два параметра: массивJdbcType[]
для указания набора JdbcType, которые обрабатываются этим TypeHandler, и булевский параметрincludeNullJdbcType
, который указывает, может ли этот TypeHandler обрабатывать null тип.
Если значение аннотации MappedJdbcTypes
найдено, используется тип Java, JdbcType и экземпляр обработчика типа для регистрации обработчика типа. Также проверяется параметр includeNullJdbcType
в аннотации MappedJdbcTypes
для определения, может ли этот TypeHandler обрабатывать null значения.
Если значение аннотации MappedJdbcTypes
не найдено, используется тип Java, null и экземпляр обработчика типа для регистрации.
В обоих этих ветвях регистрирование выполняется с помощью метода register(Type, JdbcType, TypeHandler<?>)
.Код метода register(Type, JdbcType, TypeHandler<?>)
выглядит следующим образом:
/**
* Регистрация обработчика типов
*
* @param javaType java тип
* @param jdbcType jdbc тип
* @param handler обработчик
*/
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
// Получение коллекции конвертеров jdbc типов для данного java типа
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
// Регистрация java типа и отображения [jdbc тип и обработчик] в TYPE_HANDLER_MAP
TYPE_HANDLER_MAP.put(javaType, map);
}
map.put(jdbcType, handler);
}
// Регистрация отображения типа обработчика и экземпляра обработчика
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
Если переданный Java тип не равен null, то сначала получаем коллекцию конвертеров JDBC типов для данного Java типа. Эта коллекция использует JDBC типы в качестве ключей и конвертеры в качестве значений. Если коллекция не была получена или была получена NULL_TYPE_HANDLER_MAP, то создается новая коллекция, и связь Java типа и этой коллекции регистрируется в TYPE_HANDLER_MAP. Затем в коллекции регистрируется связь JDBC типа и обработчика.Затем, независимо от того, был ли передан Java тип или нет, связь типа обработчика и экземпляра обработчика регистрируется в ALL_TYPE_HANDLERS_MAP
.
NULL_TYPE_HANDLER_MAP
— это определение пустой коллекции.
TYPE_HANDLER_MAP
— это коллекция, которая хранит отображение Java типа и [JDBC типа и обработчика].
ALL_TYPE_HANDLERS_MAP
— это коллекция, которая хранит отображение типа обработчика и экземпляра обработчика.
Таким образом, после сложного процесса регистрации, задача регистрации обработчиков типов завершена.
Давайте еще раз рассмотрим основной процесс:
Сначала на основе базового пакета получаем все обработчики типов, которые нужно зарегистрировать. Затем используем аннотации или параметризованные типы для получения Java типа, который обрабатывается обработчиком. Затем преобразуем Java тип в Type
. Затем используем аннотации для получения JDBC типа, который обрабатывается обработчиком. И наконец, регистрируем обработчик типов.
OK, процесс анализа элемента package
завершен.
typeHandler
, прямая регистрация обработчиков типовПроцесс разбора элемента typeHandler
значительно проще по сравнению с разбором элемента package
. Сначала он получает определенные имена Java-типов, имена JDBC-типов и имена обработчиков типов на основе атрибутов javaType
, jdbcType
и handler
. Затем он делегирует методу BaseBuilder#resolve(String)
для разбора реальных Java-типов, JDBC-типов и типов обработчиков типов. После этого, на основе наличия или отсутствия Java-типов и JDBC-типов, определяется точка входа для регистрации псевдонимов.> Логика прямой регистрации обработчиков типов на основе разбора элемента TypeHandler
в основном включена в логику регистрации обработчиков на основе package
, поэтому здесь не будет повторного описания. Просто запомним основные шаги.Есть Java-тип и JDBC-тип: Прямое вызов метода register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass)
. После преобразования typeHandlerClass
в конкретный экземпляр, делегируется методу register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler)
, который преобразует Java-тип в тип Type
. В конце вызывается метод register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)
для выполнения реальной регистрации.
Есть Java-тип, но нет JDBC-типа: Вызов метода register(Class<?> javaTypeClass, Class<?> typeHandlerClass)
. После преобразования typeHandlerClass
в конкретный экземпляр, делегируется методу register(Class<T> javaType, TypeHandler<? extends T> typeHandler)
, который преобразует Java-тип в тип Type
. Затем вызывается метод register(Type javaType, TypeHandler<? extends T> typeHandler)
для получения JDBC-типа, и в конце вызывается метод register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler)
для выполнения реальной регистрации.
Нет Java-типа: Этот сценарий соответствует логике регистрации обработчиков типов на основе package
после обнаружения обработчика типов.
Таким образом, разбор элемента typeHandlers
завершен, и основные подготовительные работы для MyBatis завершены.Следующим шагом будет разбор конфигурационных файлов мапперов MyBatis и инициализация SQL-среды.
В MyBatis для конфигурации мапперов (интерфейсов, определяющих операции DAO) обычно используются два способа: на основе аннотаций и на основе XML-конфигураций. Например:
На основе аннотаций
@Select({"SELECT * FROM blog"})
@MapKey("id")
Map<Integer, Blog> selectBlogsAsMapById();
На основе XML
List<Blog> selectBlogsFromXML();
<select id="selectBlogsFromXML" resultType="org.apache.ibatis.domain.blog.Blog">
SELECT * FROM blog
</select>
Независимо от того, каким образом мы используем эти мапперы, нам необходимо указать MyBatis, где находятся эти мапперы и SQL-запросы, для этого мы используем элемент mappers
в основном конфигурационном файле MyBatis.
DTD определение элемента mappers
выглядит следующим образом:
<!ELEMENT mappers (mapper*, package*)>
Элемент mappers
позволяет содержать ноль или более подэлементов mapper
/package
.
Подэлемент package
имеет обязательное свойство name
, которое указывает на базовый пакет.
<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>
MyBatis при парсинге подэлемента package
регистрирует все подходящие мапперы в этом пакете.Подэлемент mapper
имеет три свойства, из которых одно должно быть заполнено, но не более одного.
DTD определение подэлемента mapper
выглядит следующим образом:
<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>
Где:
resource
указывает на относительный путь к ресурсу (XML ресурсу)url
указывает на полный путь к ресурсу (XML ресурсу)class
указывает на полное имя класса маппераЭти четыре способа регистрации мапперов можно разделить на два типа: на основе экземпляра маппера и на основе XML файла.
package
иclass
относятся к регистрации на основе экземпляра маппера,resource
иurl
относятся к регистрации на основе XML файла. Парсинг элементаmappers
в основном конфигурационном файле MyBatis осуществляется в методеXmlConfigBuilder#parseConfiguration(XNode root)
:
// !! Регистрация парсинга файлов MapperXml, соответствующих Dao
mapperElement(root.evalNode("mappers"));
Метод mapperElement
определен следующим образом:
/**
* Парсинг узла mappers в конфигурационном файле
*
* @param parent содержимое узла mappers
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// Парсинг элемента package, регистрация всех мапперов в пакете
// Например: <package name="org.mybatis.builder"/>
// Проверка атрибута name в configuration>mappers>package
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
// Загрузка всех DAO-классов из пакета и регистрация
configuration.addMappers(mapperPackage);
} else {
// Парсинг элемента mapper
}
}
}
}
``` // Определение атрибутов configuration>mappers>mapper
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// Парсинг ресурса с относительным классом
// Например: <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> // Только ресурс
ErrorContext.instance().resource(resource);
// Чтение указанного XML ресурса
InputStream inputStream = Resources.getResourceAsStream(resource);
// Продолжение парсинга XML файла маппера
XMLMapperBuilder mapperParser = new XMLMapperBuilder(
inputStream, /*Входной поток XML файла*/
configuration, /*Глобальная конфигурация пользователя*/
resource, /*Адрес конфигурационного файла ресурса*/
configuration.getSqlFragments() /*Уже существующие XML фрагменты кода*/
);
/*Парсинг этого XML файла*/
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// Парсинг ресурса с полным идентификатором ресурса
// Например: <mapper url="file:///var/mappers/AuthorMapper.xml"/>
}```markdown
// Только url
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) {
// Парсинг ресурса с полным именем класса реализации маппера
// Например: <mapper class="org.mybatis.builder.AuthorMapper"/>
}
```markdown
// Только mapperClass, добавляем объект напрямую
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
// Одновременное указание нескольких элементов недопустимо
throw new BuilderException("Элемент mapper может указывать только на url, resource или class, но не более одного.");
}
}
}
После получения всех дочерних узлов элемента mappers
, MyBatis выполняет различные логические операции в зависимости от типа дочерних элементов mappers
.
```其中 package
元素会交给 `Configuration#addMappers(String)` 来完成映射器的批量注册。
// Парсинг package элемента, регистрируем все маппера из пакета
// Например: <package name="org.mybatis.builder"/>
// Проверяем атрибут configuration>mappers>package
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
// Загружаем все DAO-классы из пакета и регистрируем их
configuration.addMappers(mapperPackage);
}
````mapper` элемент выполняет различные методы регистрации в зависимости от существующих атрибутов. Если присутствует только атрибут `class`, то вызывается метод `Configuration#addMapper(Class<T> type)` для регистрации маппера.
```java
if (resource == null && url == null && mapperClass != null) {
// Парсинг полного имени класса, реализующего интерфейс маппера
// Например: <mapper class="org.mybatis.builder.AuthorMapper"/>
// Если указан только mapperClass, добавляем объект напрямую
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
}
Обработка атрибутов resource
и url
отличается только способом загрузки XML-файлов. После получения входного потока XML-файла, MyBatis создает объект XMLMapperBuilder
для регистрации маппера.
resource
:if (resource != null && url == null && mapperClass == null) {
// Парсинг ссылки на ресурс по относительному пути класса
// Например: <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
``` // Только ресурс
ErrorContext.instance().resource(resource);
// Чтение указанного XML-ресурсного файла
InputStream inputStream = Resources.getResourceAsStream(resource);
// Продолжение парсинга файла Mapper XML
XMLMapperBuilder mapperParser = new XMLMapperBuilder(
inputStream, /*XML файловый вводный поток*/
configuration, /*глобальная конфигурация пользователя*/
resource, /*адрес конфигурационного файла ресурсов*/
configuration.getSqlFragments() /*уже существующие XML блоки кода*/
);
/*Парсинг данного XML файла*/
mapperParser.parse();
}
url
if (resource == null && url != null && mapperClass == null) {
// Парсинг ресурса, используя полное имя ресурса
// Например: <mapper url="file:///var/mappers/AuthorMapper.xml"/>
``` // Только URL
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(
inputStream /*XML файловый вводный поток*/,
configuration /*глобальная конфигурация пользователя*/,
url /*адрес конфигурационного файла ресурсов*/,
configuration.getSqlFragments() /*уже существующие XML блоки кода*/
);
/*Парсинг данного XML файла*/
mapperParser.parse();
}
Обратите внимание на обработку элементов resource
и url
в приведенном выше коде. Оба метода практически идентичны, отличие заключается только в способе загрузки файлов:resource
загружает XML файл с помощью классового пути загрузки: InputStream inputStream = Resources.getResourceAsStream(resource);
в то время как url
загружает XML файл с использованием URL: InputStream inputStream = Resources.getUrlAsStream(url);
.
Оба метода используют Resources
для загрузки XML ресурсного файла, Resources
представляет собой утилиту, созданную Mybatis для упрощения доступа к ресурсам.
Возвращаясь к основной логике кода, рассмотрим процесс загрузки элементов package
и mapper#class
.
В методе configuration.addMappers(mapperPackage);
объект Configuration
делегирует массовую регистрацию отображений объекту Configuration#mapperRegistry
.
Атрибут Configuration#mapperRegistry
представляет собой экземпляр класса MapperRegistry
.
MapperRegistry
представляет собой регистратор отображений (DAO объектов) в Mybatis, который также отвечает за регистрацию отображений.При парсинге элемента package
в методе Configuration#addMappers(String package)
объект Configuration
передает задачу объекту MapperRegistry
через метод addMappers(String packageName)
. В методе addMappers(String packageName)
MyBatis ограничивает классы-мапперы требованием реализовывать/наследоваться от класса Object
. Это означает, что при загрузке мапперов все Java-классы в указанном пакете будут загружены как кандидаты на роль мапперов.
/**
* Регистрация всех мапперов в указанном пакете
* @since 3.2.2
*/
public void addMappers(String packageName) {
// Регистрация всех Java-классов в указанном пакете
addMappers(packageName, Object.class);
}
В методе addMappers(String packageName, Class<?> superType)
MyBatis использует класс-инструмент ResolverUtil
для поиска всех подклассов Object
в указанном пакете. Затем все найденные подклассы передаются методу addMapper(mapperClass)
для регистрации мапперов.
/**
* Регистрация всех мапперов в указанном пакете, наследующихся от указанного типа
* @since 3.2.2
*/
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// Поиск всех классов, наследующихся от указанного типа в указанном пакете
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<?>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
// Выполнение регистрации маппера
addMapper(mapperClass);
}
}
В методе addMapper(mapperClass)
сначала исключаются классы, которые не являются интерфейсами.Затем проверяется, зарегистрирован ли уже маппер для данного класса в текущем регистре. Если маппер не зарегистрирован, выполняется его анализ и регистрация.
MapperRegistry
используется для поддержания реестра всех мапперов, который на самом деле представляет собой коллекцию карт с именемknownMappers
, хранящую отображение типов мапперов и экземпляров прокси.Перед разбором, текущий маппер добавляется в реестрknownMappers
вMapperRegistry
, чтобы избежать повторного разбора.
Затем MapperRegistry
создаёт объект MapperAnnotationBuilder
для продолжения процесса разбора и загрузки мапперов.
Если разбор и загрузка маппера в MapperAnnotationBuilder
завершаются неудачей, MapperRegistry
удаляет отображение этого маппера из реестра knownMappers
.
Обратите внимание на следующий метод, разбор элемента mapper#class
также делегируется этому методу.
/**
* Загружает маппер указанного типа
* @param type тип маппера
*/
public <T> void addMapper(Class<T> type) {
``` if (type.isInterface()) {
// Обрабатываем только интерфейсы
if (hasMapper(type)) {
// Обрабатываем только один раз
throw new BindingException("Тип " + type + " уже известен MapperRegistry.");
}
boolean loadCompleted = false;
try {
// Добавляем в известные прокси-мапперы, интерфейс маппера -> MapperProxyFactory прокси-объект
knownMappers.put(type, new MapperProxyFactory<T>(type));
// Важно добавить тип до запуска парсера.
// В противном случае, парсер маппера может попытаться автоматически привязаться.
// Если тип известен, попытка привязки аннотаций не будет предпринята
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// Выполняем разбор
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
// Удаляем интерфейс, если разбор завершился ошибкой
knownMappers.remove(type);
}
}
}
}
Для разбора элемента mapper#class
метод configuration.addMapper(mapperInterface)
делегируется методу addMapper(Class<T> type)
объекта MapperRegistry
.Таким образом, видно, что операции, выполненные перед разбором элемента package
, имеют цель только для получения всех типов мапперов, которые необходимо зарегистрировать. После получения типа маппера, реальная работа по парсингу полностью передается классу MapperAnnotationBuilder
. В данном контексте мы не будем углубляться в процесс парсинга MapperAnnotationBuilder
, а вернемся к анализу элементов resource
и url
в контексте регистрации на основе XML-файлов.
resource
и url
Ранее мы упоминали, что код для парсинга resource
и url
практически идентичен, отличие заключается в том, что один из них использует относительный путь для получения XML-файлов с помощью класс-лоадера, а другой — URL для получения XML-файлов.
Оба элемента проходят через одинаковый процесс: получение значений resource
/url
, передача этих значений объекту Resources
для получения входного потока XML-файла, а затем создание объекта XmlMapperBuilder
для парсинга файла маппера.
XMLMapperBuilder mapperParser = new XMLMapperBuilder(
inputStream /*входной поток XML-файла*/,
configuration /*глобальная конфигурация MyBatis*/,
url /*адрес конфигурационного файла*/,
configuration.getSqlFragments() /*уже существующие XML-блоки*/
);
В конструкторе XmlMapperBuilder
единственное, что может быть не знакомо, это configuration.getSqlFragments()
. Configuration#sqlFragments
представляет собой коллекцию, используемую для хранения блоков кода, и ее роль будет подробно объяснена в процессе дальнейшего парсинга.XmlMapperBuilder
является реализацией класса BaseBuilder
и предназначен для парсинга XML-файлов маппера MyBatis.
В процессе создания XmlMapperBuilder
создается экземпляр парсера XPathParser
.
Парсер
XPathParser
мы упоминали ранее. Он использует SAX для парсинга XML-документа, представленного входным потоком, и сохраняет его в виде DOM-дерева в объектеXPathParser
.
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()), // Создание парсера XPath
configuration, // Конфигурация Mybatis
resource, // Путь к ресурсу
sqlFragments // Существующие блоки SQL
);
}
Затем продолжается инициализация объекта XmlMapperBuilder
путем вызова других конструкторов:
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource); // Создание помощника для парсинга Mapper файла
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
MapperBuilderAssistant
является инструментом-помощником для парсинга файлов mapper
, фактически большинство операций по парсингу файлов mapper
выполняются именно этим классом.
MapperBuilderAssistant
также является экземпляром классаBaseBuilder
, более подробные сведения оMapperBuilderAssistant
будут рассмотрены позже.После завершения конструированияXmlMapperBuilder
MyBatis начинает вызывать методXmlMapperBuilder#parse()
для парсинга файлаmapper
.
/* Парсинг данного XML файла */
mapperParser.parse();
В методе parse()
класса XmlMapperBuilder
сначала вызывается метод isResourceLoaded(String resource)
объекта Configuration
для проверки, был ли уже обработан указанный ресурс. Если ресурс еще не был обработан, будет выполнен полный процесс парсинга.> Атрибут Set<String> loadedResources
объекта Configuration
используется для поддержания коллекции всех уже загруженных ресурсов, основная цель — предотвратить повторное парсинг mapper
файла.
Если указанный файл mapper
еще не был обработан, MyBatis использует парсер XPathParser
для парсинга всех элементов под корневым элементом mapper
файла mapper
в объекты XNode
, чтобы завершить парсинг XML файла.
/**
* Входной метод для парсинга файла MapperXml
*/
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// Полный процесс парсинга будет выполнен только при первой загрузке конфигурационного файла
// Чтение и конфигурация содержимого файла MapperXml
configurationElement(parser.evalNode("/mapper"));
// Запись текущего загруженного конфигурационного файла
configuration.addLoadedResource(resource);
// Привязка интерфейса DAO и текущего конфигурационного файла
bindMapperForNamespace();
}
// Парсинг необработанных ResultMap
parsePendingResultMaps();
// Парсинг необработанных ссылок на кэш
parsePendingCacheRefs();
// Парсинг необработанных операторов
parsePendingStatements();
}
После получения объекта XNode
, соответствующего файлу mapper
, MyBatis начинает вызывать метод configurationElement(XNode context)
для парсинга объекта XNode
, чтобы завершить конфигурацию одного маппера.Корневой элемент файла mapper
MyBatis имеет имя mapper
и обязательное свойство namespace
. Это свойство используется для указания пространства имен, соответствующего текущему файлу mapper
. Значение этого пространства имен уникально во всем MyBatis. Обычно мы задаем его как полное имя класса интерфейса маппера, и MyBatis автоматически связывает содержимое файла mapper
с интерфейсом маппера (DAO). Элемент mapper
содержит девять подэлементов:
cache-ref
конфигурация ссылки на кэш в других пространствах имен.cache
конфигурация кэша.resultMap
конфигурация коллекции отображений результатов, определяющая, как значения столбцов базы данных преобразуются в объекты Java.parameterMap
конфигурация коллекции отображений параметров запроса, этот параметр сейчас устарел.sql
определение блока SQL-кода, который может использоваться для ссылки другими выражениями.insert
определение вставочного выражения.update
определение выражения обновления.delete
определение выражения удаления.select
определение выражения выборки.Процесс разбора этих подэлементов и их конкретные функции будут подробно объяснены в последующих статьях.Сначала рассмотрим разбор элемента mapper
.
При разборе элемента mapper
, MyBatis сначала получает значение атрибута namespace
(пространство имен) текущего элемента mapper
. Значение namespace
очень важно, так как оно используется для уникальной идентификации элемента mapper
в среде MyBatis. Также уникальная идентификация всех подэлементов mapper
зависит от этого значения. Именно благодаря наличию namespace
, MyBatis обеспечивает возможность кросс-файловых ссылок.
Получив значение namespace
, MyBatis использует его для инициализации пространства имен MapperBuilderAssistant
, связанного с XmlMapperBuilder
. Последующий процесс разбора будет зависеть от этого значения.
/**
* Разбор конфигурации элемента mapper
*
* @param context элемент mapper
*/
private void configurationElement(XNode context) {
try {
// Получение текущего пространства имен (рабочего пространства) конфигурационного файла, обычно это полное имя класса DAO
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Пространство имен маппера не может быть пустым");
}
// Конфигурация текущего пространства имен (рабочего пространства)
builderAssistant.setCurrentNamespace(namespace);
} catch (Exception e) {
throw new BuilderException("Error configuring Mapper 'namespace'", e);
}
}
``` // Разбор элемента ссылки на кэш
cacheRefElement(context.evalNode("cache-ref"));
// Разбор конфигурации кэша и установка кэша для текущего пространства имен, по умолчанию MyBatis использует PerpetualCache
cacheElement(context.evalNode("cache"));
// Разбор и регистрация элемента parameterMap
parameterMapElement(context.evalNodes("/mapper/parameterMap")); // Разбор и регистрация элемента resultMap
resultMapElements(context.evalNodes("/mapper/resultMap"));
```markdown
// Парсинг и регистрация элемента Sql, здесь просто все фрагменты SQL читаются и помещаются в {@link Configuration#sqlFragments},
// не выполняются дополнительные операции.
sqlElement(context.evalNodes("/mapper/sql"));
// Построение объявления операций CRUD
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Ошибка при парсинге XML-файла маппера. Путь к файлу: '" + resource + "'. Причина: " + e, e);
}
}
В методе builderAssistant.setCurrentNamespace(namespace)
производится базовая проверка значения пространства имен:
/**
* Установка текущего пространства имен для помощника парсинга Mybatis Mapper.
* @param currentNamespace пространство имен
*/
public void setCurrentNamespace(String currentNamespace) {
if (currentNamespace == null) {
throw new BuilderException("Элемент маппера требует указания атрибута namespace.");
}
// Проверка пространства имен, если текущее пространство имен уже определено и не совпадает с переданным пространством имен, будет выброшено исключение.
if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
throw new BuilderException("Неправильное пространство имен. Ожидается '" + this.currentNamespace + "' но найдено '" + currentNamespace + "'.");
}
}
``` // Установка значения
this.currentNamespace = currentNamespace;
}
После завершения обработки пространства имен MyBatis сначала парсит содержимое элемента <cache-ref>
.Перед этим стоит ознакомиться с некоторыми основными аспектами MapperBuilderAssistant
помощника парсинга Mapper:
/**
* Текущее пространство имен для парсинга XML-файла маппера (рабочее пространство)
*/
private String currentNamespace;
/**
* Соответствующий ресурсный файл
*/
private final String resource;
/**
* Текущий используемый кэш
*/
private Cache currentCache;
/**
* Существует ли необработанный элемент cache-ref
*/
private boolean unresolvedCacheRef; // issue #676
У этого помощника много методов, но параметров немного, всего пять параметров:
```- currentNamespace
глобально уникальный идентификатор XML-файла, который анализирует текущий помощник
resource
путь к XML-файлу, который анализирует текущий помощникcurrentCache
объект кэша, используемый текущим помощником во время анализа XML-файлаunresolvedCacheRef
наличие неразрешимых ссылок на кэш во время анализа текущим помощникомМожно приступить к анализу процесса обработки элементов в файле маппера Mybatis.
Элемент cache-ref
в Mybatis используется для конфигурирования ссылки на объекты кэша из других пространств имен. Подробное описание объектов кэша будет дано в последующих статьях.DTD определение элемента cache-ref
выглядит следующим образом:
<!ELEMENT cache-ref EMPTY>
<!ATTLIST cache-ref
namespace CDATA #REQUIRED
>
Элемент cache-ref
имеет только один обязательный атрибут namespace
. Значение этого атрибута представляет собой уникальный по всему приложению идентификатор объекта кэша, определенного в MyBatis. Методы генерации этого идентификатора будут описаны в последующих статьях.Анализ элемента cache-ref
является относительно простым. MyBatis использует XPathParser
, чтобы получить определение XNode
элемента cache-ref
из элемента mapper
. Затем текущее пространство имен и уникальный идентификатор объекта кэша регистрируются в таблице регистрации ссылок на кэш cacheRefMap
в конфигурации Configuration
. Затем создается экземпляр CacheRefResolver
с использованием текущего помощника построения маппера MapperBuilderAssistant
и уникального идентификатора объекта кэша. Затем через метод resolveCacheRef()
экземпляра CacheRefResolver
завершается обработка ссылки на кэш.Если в методе resolveCacheRef()
возникает исключение незавершенной обработки элемента (IncompleteElementException
), то в конфигурации Configuration
регистрируется экземпляр незавершенного CacheRefResolver
.
// Парсинг кэш-ссылок
cacheRefElement(context.evalNode("cache-ref"));
/**
* Парсинг узла cache-ref
*
* @param context узел cache-ref
*/
private void cacheRefElement(XNode context) {
if (context != null) {
// Обработка ссылок на кэши других пространств имен
// Добавление кэш-ссылок
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
// Создание объекта кэш-ссылки
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// Парсинг ссылок на кэши, установка используемого кэша для builderAssistant
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
// Если ссылка на кэш не существует (возможно, еще не загружена), добавляется метка незавершенной ссылки, которая будет завершена позже
// Добавление метки незавершенной кэш-ссылки
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
В приведенном выше тексте упоминается кэш-ссылочный парсер CacheRefResolver
. CacheRefResolver
является относительно простым объектом, который имеет два свойства и два метода. Среди свойств MapperBuilderAssistant assistant
представляет собой помощника построения отображений для текущего XML-файла, а String cacheRefNamespace
представляет собой уникальный идентификатор для ссылки на кэш.```Эти два параметра передаются в конструктор, а оставшийся метод для разрешения кеша ссылающихся объектов resolveCacheRef()
делегирует реальную операцию разрешения методу `MapperBuilderAssistant` `useCacheRef(String namespace)`.``````markdown
/**
В методе `MapperBuilderAssistant` `useCacheRef(String namespace)` MyBatis извлекает объект кеша из таблицы регистрации кешей `caches` объекта конфигурации `Configuration`. Если объект кеша существует, он присваивается текущему используемому полю кеша `currentCache` вспомогательного объекта `MapperBuilderAssistant`, а `unresolvedCacheRef` устанавливается в `true`, что означает, что объект кеша был получен. Если объект кеша не был получен, `unresolvedCacheRef` устанавливается в `false`, и выбрасывается исключение незавершенного элемента (`IncompleteElementException`).
Таким образом, разрешение ссылок на кеш завершено, и теперь следует разбор конфигурации кеша.
В XML-файле маппера MyBatis конфигурация кеша задается элементом `cache`, DTD определение элемента `cache` представлено ниже:
```Элемент cache
может содержать ноль или более элементов `property`, а также имеет шесть необязательных атрибутов:
type
указывает тип используемого объекта кеша.eviction
указывает стратегию очистки кеша.flushInterval
указывает интервал времени для сброса кеша, измеряется в миллисекундах.size
указывает размер содержимого кеша.readOnly
указывает, является ли кеш только для чтения.blocking
указывает, является ли кеш блокирующим.Коллекция элементов property
используется для конфигурации пользовательских настроек кеша.Для разбора элемента cache
, в методе XMLMapperBuilder#configurationElement(XNode context)
вызывается cacheElement(context.evalNode("cache"))
для начала разбора.
Метод cacheElement(XNode context)
сначала получает атрибут type
элемента cache
, который используется для маркировки типа кеша, используемого текущим маппером. Значение type
может использовать механизм псевдонимов MyBatis, и в конструкторе Configuration
регистрируется постоянный кеш с именем PERPETUAL
. После получения имени типа кэша, используется TypeAliasRegistry#resolveAlias(String string)
для разрешения типа экземпляра кэша. По умолчанию используется кэш с именем PERPETUAL
(вечный кэш).После определения типа кэша, MyBatis получает значение свойства eviction
для кэша, которое используется для определения механизма очистки кэша. Также значение eviction
может использовать механизм псевдонимов MyBatis. В конструкторе без параметров класса Configuration
по умолчанию предоставляются четыре механизма очистки кэша и их псевдонимы:
LRU
, Least Recently Used (наименее недавно используемый): удаляет объекты, которые дольше всего не использовались.FIFO
, First In First Out (первый вошел, первый вышел): удаляет объекты в порядке их добавления в кэш.SOFT
, Soft Reference (мягкая ссылка): удаляет объекты на основе состояния сборщика мусора и правил мягких ссылок.WEAK
, Weak Reference (слабая ссылка): более активно удаляет объекты на основе состояния сборщика мусора и правил слабых ссылок.После получения имени типа механизма очистки кэша, используется TypeAliasRegistry#resolveAlias(String string)
для разрешения типа экземпляра механизма очистки кэша. По умолчанию используется механизм очистки кэша с именем LRU
.После определения типа кэша и механизма очистки кэша, начинается последовательное чтение параметров.
Сначала получается значение конфигурации flushInterval
, определяющее интервал обновления кэша. Затем получается значение свойства size
, определяющее размер памяти, доступной для кэша. Затем получается значение свойства readOnly
, определяющее тип кэша (читаемый или нечитаемый), по умолчанию установлено значение false
. Затем получается значение свойства blocking
, определяющее блокирующие свойства кэша, по умолчанию установлено значение false
. В конце концов, получается пользовательское свойство property
для кэша.
Важно отметить, что для свойства
readOnly
используется отрицательный оператор (!), так как это свойство фактически соответствует свойствуreadWrite
классаCacheBuilder
, о котором будет сказано позже.После получения этих свойств, они передаются в методuseNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props)
помощника построения маппераMapperBuilderAssistant
для настройки кэша. ``` /**
### Конфигурация пользовательских параметров```java
// Конфигурация пользовательских параметров
Properties props = context.getChildrenAsProperties();
// Использование нового кеша
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
useNewCache
класса MapperBuilderAssistant
Метод useNewCache
класса MapperBuilderAssistant
создаёт новый кеш с использованием переданных параметров. После этого MyBatis регистрирует созданный кеш в таблице кешей (caches
) конфигурации. Затем созданный кеш присваивается свойству currentCache
текущего объекта MapperBuilderAssistant
. Это означает, что кеш, определённый с помощью тега cache
, имеет более высокий приоритет по сравнению с кешем, определённым с помощью тега cache-ref
.
Конечно, это не означает, что кеш, определённый с помощью тега cache-ref
, никогда не будет использоваться. В методе cacheElement(XNode context)
есть проверка на наличие конфигурации тега cache
.
private void cacheElement(XNode context) {
if (context != null) {
// Обработка элемента кеша...
}
}
CacheBuilder
Кеш фактически создаётся с помощью CacheBuilder
. CacheBuilder
— это класс-строитель, используемый для создания объектов типа Cache
. У этого класса есть следующие свойства:```java
/**
Эти свойства соответствуют параметрам, которые можно использовать при конфигурации `CacheBuilder`.
Конечно, основной код всё ещё находится в методе build()
. Давайте вспомним код, использующий CacheBuilder
в помощнике построения маппера, и я объясню, какую роль играет каждый параметр:/ Регистрирует новый кэш для текущего пространства имен
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class)) /* Конфигурация экземпляра кэша, по умолчанию PerpetualCache.class */
.addDecorator(valueOrDefault(evictionClass, LruCache.class)) /* Конфигурация стратегии вытеснения, по умолчанию LruCache.class */
.clearInterval(flushInterval) /* Конфигурация интервала очистки кэша */
.size(size) /* Конфигурация размера памяти, доступной для кэша */
.readWrite(readWrite) /* Конфигурация способности кэша к чтению и записи */
.blocking(blocking) /* Конфигурация блокировки кэша */
.properties(props) /* Конфигурация пользовательских свойств кэша */
.build();
Теперь давайте рассмотрим процесс построения объекта Cache
. В методе build()
класса CacheBuilder
сначала вызывается метод setDefaultImplementations()
, который конфигурирует дефолтные реализации кэша. Если пользователь не указал конкретную реализацию кэша, то используется PerpetualCache
. Если пользователь не указал стратегию очистки кэша, то используется LruCache
.
/**
* Конфигурирует дефолтные реализации кэша
*/
private void setDefaultImplementations() {
if (implementation == null) {
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
// Добавляем дефолтную стратегию очистки кэша
decorators.add(LruCache.class);
}
}
}
```После настройки дефолтных реализаций кэша, через рефлексию создается экземпляр класса кэша. Класс кэша должен иметь конструктор с одним параметром типа `String`. Затем через рефлексию пользовательские параметры передаются в экземпляр кэша. Определение того, являются ли параметры валидными, зависит от наличия соответствующих методов установки параметров в экземпляре кэша.
После внедрения пользовательских параметров в экземпляр кэша, производится обертывание экземпляра кэша. Для `PerpetualCache` используется текущий набор оберток `decorators` для обертывания экземпляра кэша. Каждая обертка пытается внедрить пользовательские параметры. После завершения обертывания с помощью `decorators`, производится дополнительное обертывание в соответствии с параметрами, указанными при создании `CacheBuilder`.
- Если пользователь указал параметр `size` и в кэше есть метод установки параметра `size`, то значение параметра `size` устанавливается.
- Если пользователь указал параметр `clearInterval`, то кэш обертывается оберткой с функцией периодической очистки кэша, интервал очистки равен значению параметра `clearInterval`.
- Если пользователь указал параметр `readWrite`, то кэш обертывается оберткой с функцией сериализации.
- Кэш обертывается оберткой с функцией логирования.- Кэш обёртывается обёрткой с функцией синхронизации.
- Если пользователь указал параметр `blocking` со значением `true`, то методы кэша становятся блокирующими. После завершения процесса упаковки возвращается упакованный экземпляр кэша `PerpetualCache`.
> Если вы не знакомы с концепцией упаковки обёртки, рекомендуется ознакомиться с паттерном декоратора в проектировании программного обеспечения.Для кэшей, которые не являются экземплярами `PerpetualCache` и не являются `LoggingCache`, будет добавлена обёртка с функцией логирования. До этого момента создание экземпляра кэша было завершено.
```java
public Cache build() {
// Установка по умолчанию реализации кэша
setDefaultImplementations();
// Получение экземпляра кэша с помощью рефлексии и настройка его уникального идентификатора
Cache cache = newBaseCacheInstance(implementation, id);
// Настройка пользовательских параметров кэша с помощью рефлексии
setCacheProperties(cache);
// issue #352, не применять декораторы к пользовательским кэшам
if (PerpetualCache.class.equals(cache.getClass())) {
// Добавление декораторов кэша
for (Class<? extends Cache> decorator : decorators) {
// Экземпляр декоратора кэша
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// Добавление стандартных декораторов к кэшу
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// Если кэш не имеет функции логирования, добавляем её
cache = new LoggingCache(cache);
}
return cache;
}
```
Метод для стандартной настройки декораторов для `PerpetualCache`:
```java
private Cache setStandardDecorators(Cache cache) {
try {
// Получение метаданных для конфигурирования кэша
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
// Установка допустимого размера кэша
metaCache.setValue("size", size);
}
if (clearInterval != null) {
// Установка интервала очистки кэша
cache = new ScheduledCache(cache);
}
}
return cache;
}
``` ((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
// Установка кэша с возможностью чтения и записи
cache = new SerializedCache(cache);
}
// Добавление функции логирования
cache = new LoggingCache(cache);
// Добавление функции синхронизации
cache = new SynchronizedCache(cache);
if (blocking) {
// Добавление блокировки для методов кэша
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Ошибка при настройке стандартных декораторов кэша. Причина: " + e, e);
}
}
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )