Prospect review
In the MVP version, we have implemented a basic running mybatis.
As the saying goes, everything is difficult at the beginning, then difficult in the middle.
The plug-in mechanism of mybatis is the second soul besides dynamic proxy.
Let’s experience the pain and happiness of this interesting soul~
The role of plug-ins
In the actual development process, the mybaits plug-in we often use is the paging plug-in. Through the pagination plug-in, we can get the paginated data without writing the count statement and limit, which brings us great development
The convenience of. In addition to paging, plug-in usage scenarios mainly include updating common fields of database, sub database and sub table, encryption and decryption, etc.
This blog mainly talks about the principle of the mybatis plug-in. The next blog will design a mybatis plug-in to realize the function of generating snowflake ID as the primary key of each data instead of automatically increasing ID of database when adding data.
JDK dynamic agent + responsibility chain design pattern
Mybatis’s plug-in is actually an interceptor function. It uses the comprehensive application of JDK dynamic agent and responsibility chain design pattern. Adopt the chain of responsibility model, organize multiple interceptors through dynamic agents, through which you can do something you want to do.
So before we talk about the mybatis interceptor, let’s talk about the JDK dynamic proxy + chain of responsibility design pattern.
JDK dynamic proxy case
package com.github.houbb.mybatis.plugin;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkDynamicProxy {
/**
*One interface
*/
public interface HelloService{
void sayHello();
}
/**
*The target class implements the interface
*/
static class HelloServiceImpl implements HelloService{
@Override
public void sayHello() {
System.out.println("sayHello......");
}
}
/**
*Custom proxy classes need to implement the invocationhandler interface
*/
static class HelloInvocationHandler implements InvocationHandler {
private Object target;
public HelloInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println ("--- insert pre notification code --------";
//Implement the corresponding target method
Object rs = method.invoke(target,args);
System.out.println ("--- insert post processing code --------";
return rs;
}
public static Object wrap(Object target) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),new HelloInvocationHandler(target));
}
}
public static void main(String[] args) {
HelloService proxyService = (HelloService) HelloInvocationHandler.wrap(new HelloServiceImpl());
proxyService.sayHello();
}
}
- output
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------
Optimization 1: object oriented
The above proxy function is implemented, but there is an obvious defect. Helloinvocationhandler is a dynamic proxy class, which can also be understood as a tool class. It is impossible for us to write business code to the invoke method,
Do not conform to the idea of object-oriented, can be abstracted to deal with.
Defining interfaces
You can design an interceptor interface, and you need to intercept and implement the interface.
public interface Interceptor {
/**
*Specific interception processing
*/
void intercept();
}
Implementation interface
public class LogInterceptor implements Interceptor{
@Override
public void intercept() {
System.out.println ("--- insert pre notification code --------";
}
}
and
public class TransactionInterceptor implements Interceptor{
@Override
public void intercept() {
System.out.println ("--- insert post processing code --------";
}
}
Implement proxy
public class InterfaceProxy implements InvocationHandler {
private Object target;
private List<Interceptor> interceptorList = new ArrayList<>();
public InterfaceProxy(Object target, List<Interceptor> interceptorList) {
this.target = target;
this.interceptorList = interceptorList;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//Processing multiple interceptors
for (Interceptor interceptor : interceptorList) {
interceptor.intercept();
}
return method.invoke(target, args);
}
public static Object wrap(Object target, List<Interceptor> interceptorList) {
InterfaceProxy targetProxy = new InterfaceProxy(target, interceptorList);
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), targetProxy);
}
}
Test verification
public static void main(String[] args) {
List<Interceptor> interceptorList = new ArrayList<>();
interceptorList.add(new LogInterceptor());
interceptorList.add(new TransactionInterceptor());
HelloService target = new HelloServiceImpl();
HelloService targetProxy = (HelloService) InterfaceProxy.wrap(target, interceptorList);
targetProxy.sayHello();
}
- journal
------Insert pre notification code-------------
------Insert post processing code-------------
sayHello......
There is an obvious problem here, all intercepts are handled before the method is executed.
Optimization 2: before and after flexible designation
The above dynamic proxy can indeed extract the business logic from the proxy class. However, we notice that only the front-end agent can not achieve the front-end and back-end proxy, so it needs to be optimized.
So we need to do a little more abstraction,
The information of intercepted object is encapsulated as the parameter of interceptor interception method, and the real execution method of intercepting target object is put into interceptor to complete. In this way, interception before and after can be realized, and parameters of intercepted object can be modified.
Realization ideas
Proxy class context
Design an invocation object.
public class Invocation {
/**
*Target object
*/
private Object target;
/**
*Methods of implementation
*/
private Method method;
/**
*Parameters of the method
*/
private Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
/**
*Method of executing target object
*/
public Object process() throws Exception{
return method.invoke(target,args);
}
//Omit getter / setter
}
Adjust the interface
- Interceptor.java
public interface Interceptor {
/**
*Specific interception processing
*/
Object intercept(Invocation invocation) throws Exception;
}
- Log implementation
public class MyLogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Exception {
System.out.println ("--- insert pre notification code --------";
Object result = invocation.process();
System.out.println ("--- insert post processing code --------";
return result;
}
}
Re implement the proxy class
public class MyInvocationHandler implements InvocationHandler {
private Object target;
private Interceptor interceptor;
public MyInvocationHandler(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(target, method, args);
//It still returns the result of the proxy class
return interceptor.intercept(invocation);
}
public static Object wrap(Object target, Interceptor interceptor) {
MyInvocationHandler targetProxy = new MyInvocationHandler(target, interceptor);
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
targetProxy);
}
}
The most important thing is to construct the invocation and then execute the corresponding method.
test
- code
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
Interceptor interceptor = new MyLogInterceptor();
HelloService targetProxy = (HelloService) MyInvocationHandler.wrap(target, interceptor);
targetProxy.sayHello();
}
- journal
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------
Optimization 3: clear boundaries
In this way, the front and back interception can be realized, and the interceptor can obtain the intercepted object information.
However, the call of the test code looks very awkward. For the target class, you only need to know what interception is inserted to it.
Again, add a method to insert the target class in the interceptor.
realization
Interface adjustment
public interface Interceptor {
/**
*Specific interception processing
*
*The result of @ return method execution
* @since 0.0.2
*/
Object intercept(Invocation invocation) throws Exception;
/**
*Insert target class
*
*@ return agent
* @since 0.0.2
*/
Object plugin(Object target);
}
Realize the adjustment
It can be understood as adjusting static methods to object methods.
public class MyLogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Exception {
System.out.println ("--- insert pre notification code --------";
Object result = invocation.process();
System.out.println ("--- insert post processing code --------";
return result;
}
@Override
public Object plugin(Object target) {
return MyInvocationHandler.wrap(target, this);
}
}
test
- code
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
Interceptor interceptor = new MyLogInterceptor();
HelloService targetProxy = (HelloService) interceptor.plugin(target);
targetProxy.sayHello();
}
- journal
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------
Responsibility chain model
How to deal with multiple interceptors?
Test code
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
//1. Interceptor 1
Interceptor interceptor = new MyLogInterceptor();
target = (HelloService) interceptor.plugin(target);
//2. Interceptor 2
Interceptor interceptor2 = new MyTransactionInterceptor();
target = (HelloService) interceptor2.plugin(target);
//Call
target.sayHello();
}
The implementation of mytransactioninterceptor is as follows:
public class MyTransactionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Exception {
System.out.println("------tx start-------------");
Object result = invocation.process();
System.out.println("------tx end-------------");
return result;
}
@Override
public Object plugin(Object target) {
return MyInvocationHandler.wrap(target, this);
}
}
The log is as follows:
------tx start-------------
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------
------tx end-------------
Of course, many partners have already thought of using the chain of responsibility model. Let’s take a look at the chain of responsibility model.
Responsibility chain model
Responsibility chain model
public class InterceptorChain {
private List<Interceptor> interceptorList = new ArrayList<>();
/**
*Insert all interceptors
*/
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptorList) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptorList.add(interceptor);
}
/**
*Returns a non modifiable collection that can only be added through the addinterceptor method
*It's in your own hands
*/
public List<Interceptor> getInterceptorList() {
return Collections.unmodifiableList(interceptorList);
}
}
test
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
Interceptor interceptor = new MyLogInterceptor();
Interceptor interceptor2 = new MyTransactionInterceptor();
InterceptorChain chain = new InterceptorChain();
chain.addInterceptor(interceptor);
chain.addInterceptor(interceptor2);
target = (HelloService) chain.pluginAll(target);
//Call
target.sayHello();
}
- journal
------tx start-------------
------Insert pre notification code-------------
sayHello......
------Insert post processing code-------------
------tx end-------------
Personal thinking
Can interceptors be improved?
In fact, I feel that we can change the angle here. For example, when defining the interceptor interface, it can be changed to:
In this way, the code can be implemented without writing the execution part, which is easier to implement and will not be forgotten.
public interface Interceptor {
/**
*Specific interception processing
*/
void before(Invocation invacation);
/**
*Specific interception processing
*/
void after(Invocation invacation);
}
However, there is also a disadvantage, that is, it is invisible to the part of process execution, which loses some flexibility.
Abstract implementation
For the plugin () method, the implementation is actually very fixed.
It should be invisible to the interface and can be directly put into the chain for unified processing.
Introduction of handwritten mybatis plug-in
Having said so much, if you understand, then the next plug-in implementation is a piece of cake.
It’s just a simple implementation of the above ideas.
Quick experience
config.xml
Plug in is introduced, and other parts are omitted.
<plugins>
<plugin interceptor="com.github.houbb.mybatis.plugin.SimpleLogInterceptor"/>
</plugins>
SimpleLogInterceptor.java
We simply output input and output parameters.
public class SimpleLogInterceptor implements Interceptor{
@Override
public void before(Invocation invocation) {
System.out.println("----param: " + Arrays.toString(invocation.getArgs()));
}
@Override
public void after(Invocation invocation, Object result) {
System.out.println("----result: " + result);
}
}
Execution test method
The output log is as follows.
----param: [[email protected], MapperMethod{type='select', sql='select * from user where id = ?', methodName='selectById', resultType=class com.github.houbb.mybatis.domain.User, paramType=class java.lang.Long}, [Ljava.lang.Object;@67011281]
----result: User{id=1, name='luna', password='123456'}
User{id=1, name='luna', password='123456'}
Is it simple, then how to achieve it?
Core implementation
Interface definition
public interface Interceptor {
/**
*Front intercept
*@ param invocation context
* @since 0.0.2
*/
void before(Invocation invocation);
/**
*Post intercept
*@ param invocation context
*@ param result
* @since 0.0.2
*/
void after(Invocation invocation, Object result);
}
Start plug-in
When opensession(), we start the plug-in:
public SqlSession openSession() {
Executor executor = new SimpleExecutor();
//1. Plug in
InterceptorChain interceptorChain = new InterceptorChain();
List<Interceptor> interceptors = config.getInterceptorList();
interceptorChain.add(interceptors);
executor = (Executor) interceptorChain.pluginAll(executor);
//2. Create
return new DefaultSqlSession(config, executor);
}
Here we see a chain of responsibility, which is implemented as follows.
Chain of responsibility
public class InterceptorChain {
/**
*Interceptor list
* @since 0.0.2
*/
private final List<Interceptor> interceptorList = new ArrayList<>();
/**
*Add interceptor
*@ param interceptor
* @return this
* @since 0.0.2
*/
public synchronized InterceptorChain add(Interceptor interceptor) {
interceptorList.add(interceptor);
return this;
}
/**
*Add interceptor
*Interceptor list @ interceptor list
* @return this
* @since 0.0.2
*/
public synchronized InterceptorChain add(List<Interceptor> interceptorList) {
for(Interceptor interceptor : interceptorList) {
this.add(interceptor);
}
return this;
}
/**
*Agent
*@ param target target class
*@ return result
* @since 0.0.2
*/
public Object pluginAll(Object target) {
for(Interceptor interceptor : interceptorList) {
target = DefaultInvocationHandler.proxy(target, interceptor);
}
return target;
}
}
The implementation of defaultinvocationhandler is as follows:
/**
*Default proxy implementation
* @since 0.0.2
*/
public class DefaultInvocationHandler implements InvocationHandler {
/**
*Proxy class
* @since 0.0.2
*/
private final Object target;
/**
*Interceptor
* @since 0.0.2
*/
private final Interceptor interceptor;
public DefaultInvocationHandler(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invocation = new Invocation(target, method, args);
interceptor.before(invocation);
// invoke
Object result = method.invoke(target, args);
interceptor.after(invocation, result);
return result;
}
/**
*Build agent
*@ param target target object
*@ param interceptor
*@ return agent
* @since 0.0.2
*/
public static Object proxy(Object target, Interceptor interceptor) {
DefaultInvocationHandler targetProxy = new DefaultInvocationHandler(target, interceptor);
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
targetProxy);
}
}
Summary
The implementation of this section is not difficult. It is difficult to understand the overall design concept of mybatis for plug-ins. The technical level is still dynamic agent, which combines the design mode of responsibility chain.
After learning this routine, in fact, many similar frameworks can be used for reference in our own implementation.
Extended reading
Handwritten mybatis from scratch (1) MVP version
reference material
Mybatis framework (8) — principle of mybatis plug-in (agent + chain of responsibility)