Mybatis plug-in extension and integration principle with spring

Don't talk at night 2022-02-13 07:25:55 阅读数:118

mybatis plug-in plug extension integration

Preface

The previous articles analyzed Mybatis The core principle of , But there are many modules , Not analyzed one by one , More readers need to study by themselves . however Mybatis The plug-in extension mechanism is still very important , image PageHelper Is an extension , Familiar with its expansion principle , In order to better expand our business . in addition , Now? Mybatis It's all with Spring/SpringBoot Use it together , that Mybatis How to integrate with them ? All the answers are in this article .

Text

Plug in extensions

1. Interceptor Core implementation principles

be familiar with Mybatis We all know about the configuration , stay xml In the configuration, we can configure the following nodes :

 <plugins>
<plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">
<property name="pluginProperty" value="100"/>
</plugin>
</plugins>

This is the plug-in configuration , Then naturally, this node will be parsing xml Analyze when , And add it to Configuration in . Careful readers should remember the following code , stay XMLConfigBuilderl Class :

 private void parseConfiguration(XNode root) {

try {

//issue #117 read properties first
// analysis <properties> node 
propertiesElement(root.evalNode("properties"));
// analysis <settings> node 
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
// analysis <typeAliases> node 
typeAliasesElement(root.evalNode("typeAliases"));
// analysis <plugins> node 
pluginElement(root.evalNode("plugins"));
// analysis <objectFactory> node 
objectFactoryElement(root.evalNode("objectFactory"));
// analysis <objectWrapperFactory> node 
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// analysis <reflectorFactory> node 
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);// take settings Fill in configuration
// read it after objectFactory and objectWrapperFactory issue #631
// analysis <environments> node 
environmentsElement(root.evalNode("environments"));
// analysis <databaseIdProvider> node 
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// analysis <typeHandlers> node 
typeHandlerElement(root.evalNode("typeHandlers"));
// analysis <mappers> node 
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {

throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

among pluginElement Is to parse the plug-in node :

 private void pluginElement(XNode parent) throws Exception {

if (parent != null) {

// Traverse all plug-in configurations 
for (XNode child : parent.getChildren()) {

// Get the class name of the plug-in 
String interceptor = child.getStringAttribute("interceptor");
// Get the configuration of the plug-in 
Properties properties = child.getChildrenAsProperties();
// Instantiate the plug-in object 
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
// Set plug-in properties 
interceptorInstance.setProperties(properties);
// Add plug-ins to configuration object , Bottom use list Save all plug-ins and record the order 
configuration.addInterceptor(interceptorInstance);
}
}
}

You can see from above , Is instantiated as... According to the configuration Interceptor object , To add to InterceptorChain in , The object of this class is Configuration hold .Interceptor There are three methods :

 // The way to execute the interception logic 
Object intercept(Invocation invocation) throws Throwable;
//target Is the intercepted object , Its function is to generate a proxy object for the intercepted object 
Object plugin(Object target);
// Read in plugin Parameters set in 
void setProperties(Properties properties);

and InterceptorChain Just saved all Interceptor, And provide methods for the client to call , Make all of Interceptor Generate Proxy object

public class InterceptorChain {

private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {

for (Interceptor interceptor : interceptors) {

target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {

interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {

return Collections.unmodifiableList(interceptors);
}
}

You can see pluginAll Just loop to call Interceptor Of plugin Method , The implementation of this method is generally through Plugin.wrap To generate proxy objects :

 public static Object wrap(Object target, Interceptor interceptor) {

// analysis Interceptor On @Intercepts Annotation signature Information 
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();// Get the type of the target object 
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);// Get the interface implemented by the target object 
if (interfaces.length > 0) {

// Use jdk To create a dynamic proxy 
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

among getSignatureMap Will be @Intercepts In the annotations value Values are parsed and cached , The value of this annotation is @Signature An array of types , And this annotation can define class type Method Parameters , namely Location of interceptor . and getAllInterfaces Is to get the interface to be proxied , And then through JDK Dynamic proxy creates proxy objects , You can see InvocationHandler Namely Plugin class , So just look invoke Method , The end result is a call interceptor.intercept Method :

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

try {

// Get the method that the current interface can be intercepted 
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// If the current method needs to be intercepted , Call interceptor.intercept Method to intercept 
return interceptor.intercept(new Invocation(target, method, args));
}
// If the current method does not need to be intercepted , Then call the method of the object itself 
return method.invoke(target, args);
} catch (Exception e) {

throw ExceptionUtil.unwrapThrowable(e);
}
}

The plug-in implementation idea here is general , That is this. interceptor Any method we can use to extend any object , For example, yes. Map Of get To intercept , It can be implemented as follows :

 @Intercepts({

@Signature(type = Map.class, method = "get", args = {
Object.class})})
public static class AlwaysMapPlugin implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {

return "Always";
}
@Override
public Object plugin(Object target) {

return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {

}
}

And then use Map First use the plug-in to package it , So what you get is Map Proxy object of .

 Map map = new HashMap();
map = (Map) new AlwaysMapPlugin().plugin(map);

2. Mybatis Interception enhancement

Because we can treat Mybatis Extend any number of plug-ins , So it uses InterceptorChain Object to save all plug-ins , This is a The chain of responsibility model The implementation of the . that Mybatis Which objects and methods will be intercepted ? Recalling the last article, we can find that Mybatis Only for the following 4 Intercept objects :

  • Executor
 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {

...... Omit
// adopt interceptorChain Traverse all plug-ins as executor enhance , Add the function of plug-in 
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
  • StatementHandler
 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

// establish RoutingStatementHandler object , The actual reason is statmentType To specify the real StatementHandler To achieve 
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
  • ParameterHandler
 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {

ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
  • ResultSetHandler
 public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {

ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}

The specific objects and methods to be intercepted are determined by @Intercepts and @Signature designated .

That's all Mybatis Implementation mechanism of extension , Based on this, readers can analyze PageHelper Implementation principle of . In addition, we need to pay attention to , When we are developing custom plug-ins , Be especially careful . Because it is directly related to the operation of the database , If the implementation principle of the plug-in is not thorough , It is likely to lead to incalculable consequences .

Mybatis And Spring The principle of integration

The previous examples are used alone Mybatis, You can see that you need to create SqlSessionFactory and SqlSession object , And then through SqlSession To create Mapper The proxy object of the interface , So in and Spring Integration time , obvious , We need to consider the following points :

  • When and how to create SqlSessionFactory and SqlSession
  • When and how to create proxy objects ?
  • How to integrate Mybatis The proxy object is injected into IOC In the container ?
  • Mybatis How to guarantee and Spring In the same transaction and using the same connection ?

So how to achieve the above points ? Based on mybatis-spring-1.3.3 Version analysis .

1. SqlSessionFactory The creation of

be familiar with Spring Source code ( If you're not familiar with , You can read my previous Spring Series source code ) We all know Spring The most important extension points :

  • BeanDefinitionRegistryPostProcessor:Bean Prior to instantiation call
  • BeanFactoryPostProcessor:Bean Prior to instantiation call
  • InitializingBean:Bean After instantiation, it is called
  • FactoryBean: Implement this interface instead of Spring Manage some special Bean

There's a lot more , The above list is Mybatis Integrate Spring The extension points used . First, we need to instantiate SqlSessionFactory, Instantiate the object in Mybatis In fact, it is to parse a lot of configurations and encapsulate them into the object , So we can't simply use <bean> Tag to configure , So Mybatis Implemented a class SqlSessionFactoryBean( This class is configured when we used the integration package in the past ), Before XML The configurations in are put into this class in the form of attributes :

 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="typeAliasesPackage" value="com.enjoylearning.mybatis.entity" />
<property name="mapperLocations" value="classpath:sqlmapper/*.xml" />
</bean>

Enter this class , We can see that it's done InitializingBean and FactoryBean Interface , The function of implementing the first interface is to execute immediately after the class is instantiated Configuration analysis The stage of :

 public void afterPropertiesSet() throws Exception {

notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}

The specific analysis is in buildSqlSessionFactory In the method , This method is longer , But it's not complicated , I'm not going to post the code here . The function of realizing the second interface is Spring When you get an instance of this class, you will actually pass getObject Method returns SqlSessionFactory Example , Through these two interfaces SqlSessionFactory Instantiation .

2. scanning Mapper And create a proxy object

After integration, we need to configure SqlSessionFactoryBean Outside , Also configure a class :

 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.enjoylearning.mybatis.mapper" />
</bean>

The function of this class is to scan Mapper Interface , And this class implements BeanDefinitionRegistryPostProcessor and InitializingBean, The function of implementing the second interface here is mainly to verify whether there is a configuration The path of the package to be scanned

 public void afterPropertiesSet() throws Exception {

notNull(this.basePackage, "Property 'basePackage' is required");
}

Mainly see postProcessBeanDefinitionRegistry Method :

 public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {

if (this.processPropertyPlaceHolders) {

processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

A scan class is created here , And this scanning class is inherited from Spring Of ClassPathBeanDefinitionScanner, That is, the scanned class will be encapsulated as BeanDefinition Sign up to IOC In the container :

 public int scan(String... basePackages) {

int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {

AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
public Set<BeanDefinitionHolder> doScan(String... basePackages) {

Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {

logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {

processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {

GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {

definition = (GenericBeanDefinition) holder.getBeanDefinition();
if (logger.isDebugEnabled()) {

logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName()
+ "' and '" + definition.getBeanClassName() + "' mapperInterface");
}
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
// When specifying the sqlSessionFactoryBeanName or sqlSessionFactory or sqlSessionTemplateBeanName or sqlSessionTemplate , Inject it into the mapperFactoryBean in 
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {

definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {

definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {

if (explicitFactoryUsed) {

logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {

if (explicitFactoryUsed) {

logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
// When not specified , Is automatically injected by type sqlSession
if (!explicitFactoryUsed) {

if (logger.isDebugEnabled()) {

logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}

You might wonder , Where to generate the proxy object ? Just to Mapper Interface injection to IOC What's the use ? In fact, the key code is definition.setBeanClass(this.mapperFactoryBean.getClass()), The function of this code is to put every Mapper All interfaces are converted to MapperFactoryBean type .
Why do you turn like this ? Entering this class, you will find that it also implements FactoryBean Interface , Therefore, it is natural to use it to create proxy implementation class objects :

 public T getObject() throws Exception {

return getSqlSession().getMapper(this.mapperInterface);
}

3. How to integrate Spring Business

Mybatis As a ORM frame , It has its own data source and transaction control , and Spring These two... Will also be configured , So how do you put them together ? Not in Service Class call Mapper Interface, the data source and connection are switched , That must not work .
In the use of Mybatis when , We can do it in xml Middle configuration TransactionFactory Transaction factory class , However, the default is usually used JdbcTransactionFactory, And when and Spring After integration , The default transaction factory class is changed to SpringManagedTransactionFactory. go back to SqlSessionFactoryBean How to read the configuration , In this method, there is the following code :

 if (this.transactionFactory == null) {

this.transactionFactory = new SpringManagedTransactionFactory();
}
configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

The above default creates SpringManagedTransactionFactory, At the same time, we will xml in ref Property referenced dataSource Added to the Configuration in , The factory will create the following transaction control object :

 public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {

return new SpringManagedTransaction(dataSource);
}

And this method is in DefaultSqlSessionFactory obtain SqlSession Called when :

 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

Transaction tx = null;
try {

final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {

closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {

ErrorContext.instance().reset();
}
}

This ensures that the same data source object is used , But how to ensure that you get the same connection and transaction ? The point is SpringManagedTransaction How to get the connection :

 public Connection getConnection() throws SQLException {

if (this.connection == null) {

openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {

this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
if (LOGGER.isDebugEnabled()) {

LOGGER.debug(
"JDBC Connection ["
+ this.connection
+ "] will"
+ (this.isConnectionTransactional ? " " : " not ")
+ "be managed by Spring");
}
}

This is entrusted to DataSourceUtils Get the connection :

 public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {

try {

return doGetConnection(dataSource);
}
catch (SQLException ex) {

throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {

Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {

conHolder.requested();
if (!conHolder.hasConnection()) {

logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(dataSource.getConnection());
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = dataSource.getConnection();
if (TransactionSynchronizationManager.isSynchronizationActive()) {

logger.debug("Registering transaction synchronization for JDBC Connection");
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {

holderToUse = new ConnectionHolder(con);
}
else {

holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {

TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
}

notice ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource) This code is believed to be familiar with Spring The source code has been known , I'm analyzing this Spring Transaction source code also talked about , adopt DataSource Object to get the binding of the current thread ConnectionHolder, The object is Spring It was saved when the transaction was started . thus , About Spring and Mybatis We'll find out the integration principle of , As for and SpringBoot Integration of , The reader may make his own analysis . Last , I would like to share a little expanded knowledge .

4. FactoryBean The expansion of knowledge

Many readers may not know what this interface does , It's very simple , When we have a class by Spring Instantiation is complicated , When you want to control its instantiation , You can implement the interface . The class that implements the interface will first be instantiated and put into First level cache , And when we Dependency injection The class we really want ( Such as Mapper Proxy class for interface ), It will start from First level cache Get in the FactoryBean Instance of implementation class , And determine whether FactoryBean Interface , If so, it will call getObject Method returns the instance we really want .
So if what we really want is FactoryBean How to implement the instance of the class ? Just in the incoming beanName prefix “&” A symbol is enough .

summary

This article analyzes Mybatis How to extend the plug-in and the implementation principle of the plug-in , But if not necessary , Do not add extensions , If you have to , Then be very careful . In addition, it also combines Spirng The extension point of Mybatis and Spring The integration principle of , Solved some doubts trapped in my heart for a long time , I believe that is also the doubt of most readers , A good understanding of this part is very conducive to our own understanding of Spring Expand .

copyright:author[Don't talk at night],Please bring the original link to reprint, thank you. https://en.javamana.com/2022/02/202202130725500240.html