On the source code of mybatis -- elegant and excellent skeleton

Don't talk at night 2022-02-13 07:29:01 阅读数:138

source code mybatis elegant excellent

Preface

Mybatis It's a semi-automatic ORM frame , It's at present in China Java web Mainstream of development ORM frame , Therefore, as a developer, it is very necessary to master its implementation principle , In order to better solve the problems encountered in our development ; meanwhile ,Mybatis The architecture and source code are also very elegant , A large number of design patterns are used to realize decoupling and high scalability , So the design idea , It is also very necessary for us to understand and master .(PS: This series is based on 3.5.0 Version analysis )

Excellent Mybatis skeleton

Macro design

Mybatsi Compared with Spring The source code is much simpler in terms of architecture and Implementation , All its code is in one project , There are many subcontracts under this project , Each package has a clear division of labor :
 Insert picture description here
Although there are so many modules , In fact, it only needs to be divided into three layers :
 Insert picture description here
So after layering , Is it clear , The basic support layer is the encapsulation of some general components , Like a journal 、 cache 、 Reflection 、 Data sources and so on , These modules support the implementation of core business logic , And if necessary, we can use it directly in our project , Like the reflection module is right JDK The reflection is encapsulated , Make it easier to use ; The core processing layer is Mybatis The realization of the core business of , Through the bottom support module , The configuration file and SQL analysis 、 Parameter mapping and binding 、SQL The mapping of execution and return results, the execution of extensions, and so on ; Finally, the interface layer is the service provided externally , We use Mybatis You only need to operate through this interface , There is no need to pay attention to the underlying implementation . The benefits of this layering go without saying , Make our code more concise and readable , At the same time, maintainability and scalability are also greatly improved , In addition, from the whole architecture design, we can see the embodiment of a design pattern —— Facade mode , Because the design idea of facade pattern is Provide a unified interface to the outside world , Shield the complexity of internal system implementation , So that users can easily use all functions without paying attention to the internal implementation , The architecture design here adopts such an idea . The lines , Let's see if other open source frameworks are designed like this ?

Foundation support

In understanding Mybatis After the macro architecture design , The following is a detailed analysis of the source code , First, let's look at some key basic support modules :

  • journal
  • data source
  • cache
  • Reflection

journal

Log loading

Mybatis There is no log function in itself , Instead, third-party logs are introduced , But third party logs have their own log Level ,Mybatis What needs to be solved is how to be compatible with these log components . How compatible ?Mybatis Used Adapter pattern To solve , stay logging A unified log interface is provided under the module Log Interface :

public interface Log {

boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}

You can see that all log levels are uniformly defined in this interface , The imported third-party log component only needs to implement this interface , Invokes the corresponding components of each component in each level interface. API that will do . From the class diagram below, we can see Mybatis Which three-party log components are supported : Insert picture description here
See if you have any questions , How are these third-party log components loaded ? What is the loading order ? Is it instantiated where it needs to be used ? Of course not. ,Mybatis Here is another design pattern —— Factory mode . There is a class under the log module LogFactory, The loading of logs is realized by this class , This class decouples the instantiation of logs and the use of logs :

public final class LogFactory {

public static final String MARKER = "MYBATIS";
// The construction method of the selected third-party log component adapter 
private static Constructor<? extends Log> logConstructor;
// Automatic scan log implementation , And the loading priority of the third-party log plug-in is as follows :slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
static {

tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
private LogFactory() {

// disable construction
}
public static Log getLog(Class<?> aClass) {

return getLog(aClass.getName());
}
public static Log getLog(String logger) {

try {

return logConstructor.newInstance(logger);
} catch (Throwable t) {

throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {

setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {

setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
public static synchronized void useCommonsLogging() {

setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
public static synchronized void useLog4JLogging() {

setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public static synchronized void useLog4J2Logging() {

setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
public static synchronized void useJdkLogging() {

setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
public static synchronized void useStdOutLogging() {

setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
public static synchronized void useNoLogging() {

setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
private static void tryImplementation(Runnable runnable) {

if (logConstructor == null) {
// Execute method when constructor is not empty 
try {

runnable.run();
} catch (Throwable t) {

// ignore
}
}
}
// By the appointed log Class to initialize the constructor 
private static void setImplementation(Class<? extends Log> implClass) {

try {

Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {

log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {

throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
}

Through the above code, we can clearly see the loading order of logs , And as long as any log component is successfully loaded , Other logging components will not be loaded .

Use of logs

After the log is loaded , Naturally, we should think about where we need to print logs ?Mybatis It's right in itself JDK Native JDBC Packaging and reinforcement , Therefore, the log should be printed in the following key places :

  • establish PreparedStatement and Statement When printing SQL Statement and parameter information
  • After obtaining the query result, print the result information

The question is how to gracefully enhance these methods ?Mybatis Used A dynamic proxy To achieve . Under the log module JDBC Package is the implementation of proxy class , Let's take a look at the class diagram first :
 Insert picture description here
Know what you know , Seeing these class names, we should be able to understand the functions of these classes , They are the original JDBC API The enhancement of , When calling related methods , First of all, you will enter these proxy classes invoke Method inside , In order of execution , The first thing to enter the call must be ConnectionLogger

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

// Real connection objects 
private final Connection connection;
private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {

super(statementLog, queryStack);
this.connection = conn;
}
@Override
// Enhancements to connectivity 
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {

try {

// If from Obeject The method of inheritance directly ignores 
if (Object.class.equals(method.getDeclaringClass())) {

return method.invoke(this, params);
}
// If it's a call to prepareStatement、prepareCall、createStatement Methods , Print what to do sql sentence 
// And back to prepareStatement Proxy object of , Give Way prepareStatement I also have the ability to log , Printing parameters 
if ("prepareStatement".equals(method.getName())) {

if (isDebugEnabled()) {

debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);// Print sql sentence 
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);// Create proxy object 
return stmt;
} else if ("prepareCall".equals(method.getName())) {

if (isDebugEnabled()) {

debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);// Print sql sentence 
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);// Create proxy object 
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {

Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);// Create proxy object 
return stmt;
} else {

return method.invoke(connection, params);
}
} catch (Throwable t) {

throw ExceptionUtil.unwrapThrowable(t);
}
}
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {

InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{
Connection.class}, handler);
}
public Connection getConnection() {

return connection;
}
}

from invoke In the method, we can see the main right Connection Of prepareStatementprepareCallcreateStatement The method is enhanced , Print the log and create the corresponding proxy class to return . The implementation principle of several other classes is the same , No more details here .
But there is a problem , Several other classes are called after the connection is created , Therefore, the corresponding proxy class is created by the proxy class in the previous stage , that ConnectionLogger Where was it created ? Naturally, when getting a connection , It's the connection phase when we get the business code ,Mybatis The execution phase is encapsulated one by one Excutor actuator , Detailed code analysis later .

data source

Data source creation

All data sources need to implement JDK Of DataSource Interface ,Mybatis I have implemented the data source interface , It also supports third-party data sources . Here's a look at Mybatis Internal implementation , Also, let's start with a class diagram :
 Insert picture description here
We can see from the picture DataSource The initialization of is also through Factory mode Realized , And it itself provides three data sources :

  • PooledDataSource: Data source with connection pool
  • UnpooledDataSource: Data source without connection pool
  • JNDI data source

The last one is not analyzed here .UnpooledDataSource Is an ordinary data source , The basic data source interface is realized ; and PooledDataSource Is based on UnpooledDataSource Realized , Only the connection pool function is provided on this . In addition, we need to pay attention to PooledConnection, This class is the connection object stored in the connection pool , But it is not a real connection object , Just hold the reference of the real connection , And it enhances the real connection proxy class , The following mainly analyzes the implementation principle of connection pool .

Principle of pool technology

data structure

So first of all PooledConnection It encapsulates something :

class PooledConnection implements InvocationHandler {

private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] {
 Connection.class };
private final int hashCode;
// Record the data source object where the current connection is located , This connection was created by this data source , After closing, you also return to this data source ;
private final PooledDataSource dataSource;
// Real connection objects 
private final Connection realConnection;
// Connected proxy objects 
private final Connection proxyConnection;
// The timestamp of the connection taken from the data source 
private long checkoutTimestamp;
// Timestamp of connection creation 
private long createdTimestamp;
// Timestamp of the last time the connection was used 
private long lastUsedTimestamp;
// According to the database url、 user name 、 Generate a password hash value , Uniquely identifies a connection pool 
private int connectionTypeCode;
// Is the connection valid 
private boolean valid;
/* * Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in * * @param connection - the connection that is to be presented as a pooled connection * @param dataSource - the dataSource that the connection is from */
public PooledConnection(Connection connection, PooledDataSource dataSource) {

this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
...... Omit
/* * This method is specifically used to enhance the database connect object , Check whether the connection is valid before use , Recycle the connection when closed * */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
// If the connection is called close Method , Not really closed , Instead, recycle to the connection pool 
dataSource.pushConnection(this);// adopt pooled Data source for recycling 
return null;
} else {

try {

// Check whether the current connection is valid before use 
if (!Object.class.equals(method.getDeclaringClass())) {

// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();//
}
return method.invoke(realConnection, args);
} catch (Throwable t) {

throw ExceptionUtil.unwrapThrowable(t);
}
}
}
private void checkConnection() throws SQLException {

if (!valid) {

throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
}

There are detailed comments on properties and methods , Main concern realConnection Real connected references and invoke Method enhancement . Then let's look at the implementation of connection pool , This class contains many properties :

 private final PoolState state = new PoolState(this);
// The data source really used to create the connection 
private final UnpooledDataSource dataSource;
// OPTIONAL CONFIGURATION FIELDS
// Maximum number of active connections 
protected int poolMaximumActiveConnections = 10;
// Maximum number of idle connections 
protected int poolMaximumIdleConnections = 5;
// Maximum checkout Duration ( Maximum use time )
protected int poolMaximumCheckoutTime = 20000;
// Unable to get a connection is the maximum waiting time 
protected int poolTimeToWait = 20000;
// A maximum of several invalid connections are allowed 
protected int poolMaximumLocalBadConnectionTolerance = 3;
// Test whether the connection is valid sql sentence 
protected String poolPingQuery = "NO PING QUERY SET";
// Whether to allow test connection 
protected boolean poolPingEnabled;
// Configure for a period of time , When the connection is not used during this time , To test whether the connection is valid 
protected int poolPingConnectionsNotUsedFor;
// According to the database url、 user name 、 Generate a password hash value , Uniquely identifies a connection pool , All connections generated by this connection pool will carry this value 
private int expectedConnectionTypeCode;

I believe most of the above attributes will not be unfamiliar to readers , It should be configured during development . There is a key attribute PoolState, This is the object that mainly saves Free connection and Active connection , That is, the connection pool is used to manage resources , It contains the following properties :

 protected PooledDataSource dataSource;
// Free connection pool resource collection 
protected final List<PooledConnection> idleConnections = new ArrayList<>();
// Active connection pool resource collection 
protected final List<PooledConnection> activeConnections = new ArrayList<>();
// Number of requests 
protected long requestCount = 0;
// Cumulative time to get connected 
protected long accumulatedRequestTime = 0;
// Cumulative connection time . Remove from connection to return , Calculate the time of one use ;
protected long accumulatedCheckoutTime = 0;
// Number of times the connection timed out 
protected long claimedOverdueConnectionCount = 0;
// Cumulative overtime 
protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
// Accumulated waiting time 
protected long accumulatedWaitTime = 0;
// Number of waits 
protected long hadToWaitCount = 0;
// Number of invalid connections 
protected long badConnectionCount = 0;
Get the connection

Knowing these key attributes , Let's see how to get connections from the connection pool , stay PooledDataSource There is one of them. popConnection For getting connections :

 private PooledConnection popConnection(String username, String password) throws SQLException {

boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();// Record the start timestamp of the attempt to get the connection 
int localBadConnectionCount = 0;// Initialize the number of times to get an invalid connection 
while (conn == null) {

synchronized (state) {
// Get connections must be synchronous 
if (!state.idleConnections.isEmpty()) {
// Check for free connections 
// Pool has available connection
// Have free connection to use directly 
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {

log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// No free connection 
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// Determine whether the number of active connection pools is greater than the maximum number of connections 
// No new connections can be created 
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {

log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// If it's equal to the maximum number of connections , Can't create a new connection 
// Get the first connection created 
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Check if it has been used for more than the maximum time 
// If the timeout , Make statistics on the information of overtime connection 
state.claimedOverdueConnectionCount++;// Number of timeout connections +1
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;// The cumulative timeout increases 
state.accumulatedCheckoutTime += longestCheckoutTime;// Cumulative use of connection time increases 
state.activeConnections.remove(oldestActiveConnection);// Remove... From the active queue 
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
// If the timeout connection is not committed , Then manually roll back 
try {

oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
// If an exception occurs, just log it 
/* Just log a message for debug and continue to execute the following statement like nothing happend. Wrap the bad connection with a new PooledConnection, this will help to not intterupt current executing thread and give current thread a chance to join the next competion for another valid/good database connection. At the end of this loop, bad {@link @conn} will be set as null. */
log.debug("Bad connection. Could not roll back");
}
}
// Create a new connection in the connection pool , Note that for databases , No new connection created ;
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
// Let old connections fail 
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {

log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {

// No free connection , The first connection created did not fail , Unable to create new connection , Can only block 
try {

if (!countedWait) {

state.hadToWaitCount++;// The cumulative waiting times of the connection pool plus 1
countedWait = true;
}
if (log.isDebugEnabled()) {

log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);// Block wait for a specified time 
state.accumulatedWaitTime += System.currentTimeMillis() - wt;// The cumulative waiting time increases 
} catch (InterruptedException e) {

break;
}
}
}
}
if (conn != null) {
// Get connected successfully , To test whether the connection is valid , And update Statistics 
// ping to server and check the connection is valid or not
if (conn.isValid()) {
// Check whether the connection is valid 
if (!conn.getRealConnection().getAutoCommit()) {

conn.getRealConnection().rollback();// If there is a legacy of history , Roll back 
}
// Connection pool related statistics update 
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
// If the connection is invalid 
if (log.isDebugEnabled()) {

log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;// Cumulative number of invalid connections obtained +1
localBadConnectionCount++;// The current number of invalid connections obtained +1
conn = null;
// Get invalid connection , But if it doesn't exceed the number of retries , Allow another attempt to get a connection , Otherwise, throw an exception 
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {

if (log.isDebugEnabled()) {

log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {

if (log.isDebugEnabled()) {

log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}

The logic here is relatively complex , I summarized the whole steps and drew a picture to help understand : Loop get connection , First, judge whether there is still Free connection , If there is , Direct use , And delete an idle connection ; If it doesn't exist , Give priority to Whether the maximum number of active connections has been reached . If not, create a new connection directly ; If the maximum number of active connections has been reached , From Active connection pool Take out the earliest connection , Judge If the timeout . If there is no timeout , Call wait Methods block ; If the timeout , Then the timeout connection information is counted , And connect according to the timeout Real connection Create a new connection , At the same time, let the old connection fail . After the above steps , If you get a connection , You also need to determine whether the connection is valid , A valid connection needs to roll back previously uncommitted transactions and add them to the Active connection pool , If the connection is invalid, statistics will be made and whether the number of retries has been exceeded , If not, continue to cycle to get the connection next time , Otherwise, throw an exception . After the loop is completed, return to the obtained connection .
 Insert picture description here

Recycle connection

Ordinary connections are closed directly , Recreate when needed , The connection pool needs to recycle the connections to the pool for reuse , Avoid duplicate connections and improve efficiency , stay PooledDataSource Medium pushConnection It is used to recycle the connection :

 protected void pushConnection(PooledConnection conn) throws SQLException {

synchronized (state) {
// Recycle connections must be synchronous 
state.activeConnections.remove(conn);// Remove this connection from the active connection pool 
if (conn.isValid()) {

// Determine whether the idle connection pool resources have reached the upper limit 
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {

// Not up to the limit , To recycle 
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {

conn.getRealConnection().rollback();// If there are still transactions not committed , Roll back 
}
// Based on this connection , Create a new connection resource , And refresh the connection status 
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state.idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
// Old connection failure 
conn.invalidate();
if (log.isDebugEnabled()) {

log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
// Wake up other blocked threads 
state.notifyAll();
} else {
// If the idle connection pool has reached the upper limit , Turn the connection real off 
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {

conn.getRealConnection().rollback();
}
// Close the real database connection 
conn.getRealConnection().close();
if (log.isDebugEnabled()) {

log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
// Set connection object to invalid 
conn.invalidate();
}
} else {

if (log.isDebugEnabled()) {

log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}

The logic of recycling connections is relatively simple , But there are still a few things to pay attention to : First of all, from the Active connection pool Remove the connection , And then determine Whether it is a valid connection as well as Whether the free connection pool still has a location , If it is a valid connection and the free connection pool still has a location , You need to recycle based on the current connection Real connection And create a new connection and put it into the idle connection , Then wake up the waiting thread ; If not, just close Real connection . Both branches need to roll back the uncommitted transactions in the recycled connection and invalidate the connection . If it is an invalid connection, you only need to record the number of invalid connections .
 Insert picture description here
That's all Mybatis Implementation principle of data source and connection pool , Among them, pooling technology is very important .

cache

Cache implementation

Mybatis Yes First level cache and Second level cache , The first level cache is SqlSession Grade , Can only exist in the same SqlSession In the life cycle ; The L2 cache is cross cache SqlSession, With namespace Unit . But actually Mybatis The L2 cache is very weak , There may be dirty reading , Generally not used .
but Mybatis A lot of extensions have been made to the cache , Provides protection against cache breakdown 、 Cache clear policy 、 serialize 、 Clear regularly 、 Log and other functions , The design is very elegant , So the first mock exam of this module is the design idea. . Let's first look at the structure of the package :
 Insert picture description here
As can be seen from the picture above ,Mybatis Provides a unified cache interface ,impl and decorators The package is full of its implementation classes , From the name of the package, we can think of the cache. Here is another design pattern —— Decorator mode , Use this mode dynamic You have to add a function to the cache . The real realization is impl package Under the PerpetualCache, adopt HashMap To cache data ( Will there be concurrency security problems ?),key yes CacheKey object ,value Is cached data , Why? key yes CacheKey object , Instead of a string ? Readers can think about , How to read the cache error is not determined , This class was last analyzed . and decorators Under the package are decorators with enhanced functions , This is mainly to see BlockingCache How to prevent Cache breakdown Of .

public class BlockingCache implements Cache {

// Timeout length of blocking 
private long timeout;
// The underlying object being decorated , It's usually PerpetualCache
private final Cache delegate;
// Lock object set , Granularity to key value 
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {

this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@Override
public void putObject(Object key, Object value) {

try {

delegate.putObject(key, value);
} finally {

releaseLock(key);
}
}
@Override
public Object getObject(Object key) {

acquireLock(key);// according to key Get lock object , Obtain the lock and lock it successfully , Failed to acquire lock, blocked for a period of time and try again 
Object value = delegate.getObject(key);
if (value != null) {
// Successful data acquisition , To release the lock 
releaseLock(key);
}
return value;
}
@Override
public Object removeObject(Object key) {

// despite of its name, this method is called only to release locks
releaseLock(key);
return null;
}
private ReentrantLock getLockForKey(Object key) {

ReentrantLock lock = new ReentrantLock();// Create a lock 
ReentrantLock previous = locks.putIfAbsent(key, lock);// Add a new lock to locks Collection , If the addition is successful, use the new lock , If the addition fails, use locks Lock in collection 
return previous == null ? lock : previous;
}
// according to key Get lock object , Obtain the lock and lock it successfully , Failed to acquire lock, blocked for a period of time and try again 
private void acquireLock(Object key) {

// Get lock object 
Lock lock = getLockForKey(key);
if (timeout > 0) {
// Use a lock with a timeout 
try {

boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
// If the timeout occurs, an exception is thrown 
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {

throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {
// Use a lock without a timeout 
lock.lock();
}
}
private void releaseLock(Object key) {

ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {

lock.unlock();
}
}
}

Calling getObject When getting data , First call acquireLock according to key Get the lock , If you get the lock , From PerpetualCache Get data in cache , If not, go to the database to query the data , After returning the result, add it to the cache and release the lock , Note that when you query data in the database, you are based on key Locked , So the same key Only one thread will arrive at the database query , It won't show up Cache breakdown The problem of , This idea can also be used in our project .
That's all Mybatis The idea of solving cache breakdown , Another look at a decorator SynchronizedCache, Provide the function of synchronization , The decorator is adding and deleting the cache API Plus synchronized keyword , This decorator is used to prevent concurrency security problems in the L2 cache , There is no concurrency security problem at all in L1 cache . The rest of the decorators will not be repeated here , Interested readers can make their own analysis .

CacheKey

because Mybatis There is a dynamic in SQL, So cached key You can't just use a string to represent , So pass CacheKey To encapsulate all the factors that may affect the cache , So what factors affect caching ?

  • namespace + id
  • Of the query sql
  • Parameters of the query
  • Paging information

And in the CacheKey There are the following properties in :

 private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier; // Participate in hash Calculated multiplier 
private int hashcode; //CacheKey Of hash value , stay update The real-time operation of the function 
private long checksum; // The checksum ,hash It's worth it and 
private int count; //updateList Number of elements in 
private List<Object> updateList; // The elements in this collection determine two CacheKey Whether it is equal or not 

among updateList It is used to store all factors that may affect the cache , The other values are calculated based on the objects in the attribute , Every time the structure CacheKey Object will be called update Method :

 public void update(Object object) {

// obtain object Of hash value 
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// to update count、checksum as well as hashcode Value 
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
// Add objects to updateList in 
updateList.add(object);
}

And judge two CacheKey Whether the objects are the same is through equals Method :

 public boolean equals(Object object) {

if (this == object) {
// Compare whether it is the same object 
return true;
}
if (!(object instanceof CacheKey)) {
// Whether the type is the same 
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
//hashcode Are they the same? 
return false;
}
if (checksum != cacheKey.checksum) {
//checksum Are they the same? 
return false;
}
if (count != cacheKey.count) {
//count Are they the same? 
return false;
}
// None of the above is the same , In order to compare updateList Of the elements of hash Is the value consistent 
for (int i = 0; i < updateList.size(); i++) {

Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {

return false;
}
}
return true;
}

As you can see, the method of comparing equality here is very strict , And it's very efficient , We rewrite... In the project equals Method can also refer to the implementation of this method .

Reflection

Reflection is Mybatis Top priority , By reflection Mybatis In order to realize the instantiation of objects and the assignment of attributes , also Mybatis The reflection is right JDK Encapsulation and enhancement of , Make it easier to use , Higher performance . The key categories are as follows :

  • ObjectFactory: Create... From this object POJO Class .
  • ReflectorFactory: establish Reflector Factory class .
  • Reflector:MyBatis The foundation of reflection module , Every Reflector Objects correspond to a class , The class meta information required for reflection operation is cached in it .
  • ObjectWrapper: Packaging of objects , Abstract the attribute information of the object , He defined a series of methods to query object attribute information , And how to update properties .
  • ObjectWrapperFactory: establish ObjectWrapper Factory class .
  • MetaObject: Contains the original object 、ObjectWrapper、ObjectFactory、ObjectWrapperFactory、ReflectorFactory References to , All operations of the core reflection class can be performed through this class , It's also Facade mode The implementation of the .

Because this module is only for JDK Encapsulation , Although there are many codes and classes , But it's not very complicated , I won't go into details here .

summary

This article explains Mybatis The four core modules , You can see that a lot of design patterns are used to make the code elegant and concise , High readability , At the same time, it is easy to expand , This is also the first thing we need to consider when doing the project , The code is for people to read , How to reduce the cost of reading code , Improve the quality of your code , Reduce BUG The number of , Only by learning more excellent code design ideas can we improve our own level .

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