Skip to content
JackSparrow414
Go back

Relearning MyBatis (Part 5)

Table of contents

Open Table of contents

Article body

This article analyzes how the second approach, *Mapper mapper = sqlSession.getMapper(*Mapper.class), works.

The focus here is JDK dynamic proxies. For cglib and Byte Buddy proxy examples, see the source repository.

Note: cglib is no longer recommended because it is unmaintained and has problems on JDK 17. Use Byte Buddy instead.

A typical JDK dynamic proxy consists of the following parts.

An interface, ProxyService:

public interface ProxyService {

    String print(String message);
}

Its implementation, ProxyServiceImpl:

public class ProxyServiceImpl implements ProxyService {
    @Override
    public String print(String message) {
        return message;
    }
}

And the crucial dynamic proxy handler, CommonProxyHandler:

public class CommonProxyHandler implements InvocationHandler {

    private Object target;

    public CommonProxyHandler(Object tarject) {
        this.target = tarject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target,args);
    }
}

Finally, here is how to use it:

@SpringBootTest
public class CommonProxyTest {

    @Test
    public void testCommonProxy(){
        // Use Java polymorphism to hold an instantiated ProxyServiceImpl
        ProxyService proxyService = new ProxyServiceImpl();
        // Pass the target interface implementation instance, ProxyServiceImpl, to the handler
        CommonProxyHandler commonProxyHandler = new CommonProxyHandler(proxyService);
        // First argument: the current class loader
        // Second argument: interfaces implemented by the proxied class
        // Third argument: the handler
        // Returns the proxy for ProxyServiceImpl
        ProxyService proxyInstance = (ProxyService) Proxy.newProxyInstance(CommonProxyTest.class.getClassLoader(),
                proxyService.getClass().getInterfaces(), commonProxyHandler);
        String s = proxyInstance.print("我是一个小代理");
        System.out.println(s);
    }
}

This is a simple implementation of the most common dynamic-proxy approach.

After analyzing JDK dynamic proxies in Part 4, I noticed a problem. If the proxied class is an interface implementation, I should need to implement UserMapper to use a JDK dynamic proxy. Yet MyBatis does not require us to implement Mapper interfaces.

Start debugging.

Debugging the UserMapper variable, whose actual type is a MapperProxy dynamic proxy

Before calling an interface method, we can see that the current mapper instance is of type MapperProxy. It came from sqlSession.getMapper(*Mapper.class) in the previous step, so debug into that call.

It calls getMapper on the configuration object, passing the fully qualified class name of UserMapper.

Configuration.getMapper obtaining a mapper proxy through MapperRegistry

Next, mapperRegistry.getMapper retrieves the corresponding mapperProxyFactory based on the class name and uses it to create a MapperProxy object.

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// Obtain a MapperProxyFactory instance here
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
// Begin creating a MapperProxy object here
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

Step into mapperProxyFactory.newInstance(). MapperProxy implements InvocationHandler, so it is equivalent to CommonProxyHandler above.

 protected T newInstance(MapperProxy<T> mapperProxy) {
// Does this look familiar?
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// Call the method above
    return newInstance(mapperProxy);
  }

Pay Special Attention:

Here is the key point. In the initial example, we pass an interface implementation, an Impl class, when constructing CommonPorxyHandler. Here we pass the interface itself. Inspect new MapperProxy to see this.

MapperProxy constructor accepting SqlSession, mapperInterface, and methodCache

This creates a proxy object for the UserMapper interface, rather than a proxy for the Impl class as in the usual JDK dynamic-proxy example.

P.S. An attentive reader may ask: before configuration.getMapper(), mapperResitry.getMapper, and the crucial knowMppers.get(), how did the values get added? At project startup, while parsing Mapper.xml and mybatis-config.xml, bindMapperForNamespace calls configuration.addMapper to add them. I specifically highlighted this in red in Part 3; take a look there.

After obtaining the UserMapper proxy, but before calling addUser, another question occurred to me. Part 4 states that the final target must be an interface implementation, or method.invoke() will fail. How does MyBatis avoid this? Step into it:

MapperProxy.invoke selecting the invocation path by method type and executing MapperMethod

MyBatis checks the method and does not enter method.invoke(), avoiding that error. Instead, it instantiates a MapperMethod.

MapperMethod initializing SqlCommand, with the mapper method command shown in the debugger

Using the supplied method object, it instantiates the command inside MapperMethod. Its name is the fully qualified method path.

It ultimately calls mapperMethod.execute(), which chooses how to execute based on the SQL command type:

MapperMethod.execute calling methods such as SqlSession.insert according to the SQL command type

The core call is sqlSession.insert(). It is now clear that the arguments are the method name and parameters explicitly supplied to sqlSession in our second approach. From this point, execution is identical to the first approach.

The clever part of MyBatis is that core functionality is delegated to sqlSession, while the user-facing Mapper interface is used through method arguments. It employs another form of JDK proxy—a proxy for the interface rather than for an implementation—to let developers invoke the core functionality with less code.

To be honest, only after reading the MyBatis source did I realize that JDK dynamic proxies are not limited to interface implementations. They can be used in another way. I recommend studying MapperProxy closely to see how MyBatis accepts an interface.

That concludes this rough analysis of parts of MyBatis. The small demos I wrote are on GitHub; feel free to use them. Repository


Share this post:

Continue this series

Relearning MyBatis

  1. Relearning MyBatis (Part 1)
  2. Relearning MyBatis (Part 2)
  3. Relearning MyBatis (Part 3)
  4. Relearning MyBatis (Part 4): JDK Dynamic Proxies in Detail
  5. Relearning MyBatis (Part 5)You are here

Comments

Questions, corrections, and experiences are welcome. Sign in with GitHub to comment; both language versions share this discussion.

Comments are available on the live site only.