${} string substitution is one of the most frequently used features in Mybatis. Although ${} string substitution is easy to be injected by SQL, it can be avoided by ensuring that the content is not external or by performing strong validation on the content. Let’s take a look at how mybatis implements string substitution for ${}.

A quick review of the use of ${} :

<mapper namespace="cn.xxx.test.mybatis.mapper.UserMapper">
    <select id="queryUserById" resultType="Map">
       select * from user
       <where>
           <if test="id ! = null">
               id = ${id}
           </if>
       </where>
        limit 1
    </select>
</mapper>
Copy the code

A simple query is configured in UserMapper to replace the id parameter passed directly into the query condition, specifying the value of the ID when the query is called:

List<? > datas = sqlSession.selectList("queryUserById".1);
Copy the code

Second, the DynamicContext

Mybatis uses Ognl to parse expressions in ${}, so here’s a quick review of Ognl usage.

2.1 Ognl introduction

Ognl- Object Graph Navigation Language, which customizes a set of expressions to access and manipulate objects. Using Ognl requires a context of the type Map from which values are retrieved when Ognl expressions are parsed. The context contains a root object, which is retrieved from root when specified attributes cannot be retrieved from the context.

public class OgnlTest {

    static class User {
        private String id;
        private String name;
        private String age;

        public User(String id, String name, String age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        public String getName(a) {
            return name;
        }

        public void setName(String name) {
            this.name = name; }}public static void main(String[] args) throws Exception {
        User user = new User("1"."zhangsan"."23");
        // Create a context where root is user
        OgnlContext context = (OgnlContext) Ognl.createDefaultContext(user, new DefaultMemberAccess(true));
        // Add the userCount attribute to the context
        context.put("userCount".11);
        // Get userCount, attributes in context accessed using #
        Object ans = Ognl.getValue(Ognl.parseExpression("#userCount"), context, context.getRoot());
        System.out.println("userCount = " + ans);
        // Get the age from root. Root does not use #
        ans = Ognl.getValue(Ognl.parseExpression("age"), context, context.getRoot());
        System.out.println("age = " + ans);
        // If you specify a value, the setName method is called
        ans = Ognl.getValue(Ognl.parseExpression("name=\"lisi\""), context, context.getRoot());
        System.out.println("name = "+ user.getName()); }}Copy the code

Execute output:

userCount = 11
age = 23
name = lisi
Copy the code

2.2 ContextMap

When we use the ${} expression does not contain # #, so mybatis when parsing the ${} all the properties of the obtained from the root, we first to mybatis parsing is used when the content of the root object and implementation.

Mybatis when using Ognl using root object for ContextMap org. It is apache. Ibatis. Scripting. Xmltags. DynamicContext in an inner class. ContextMap is a subclass of HashMap, meaning that the attributes of the expression in ${} are taken from ContextMap by default, so it’s worth looking at what ContextMap contains and how the attributes are returned.

ParameterMetaObject and fallbackParameterObject are ContextMap attributes. They are defined as follows:

static class ContextMap extends HashMap<String.Object> {
    // MetaObject for the argument passed in
    private final MetaObject parameterMetaObject;
    // Whether the parameter type passed in has TypeHandler
    private final boolean fallbackParameterObject;

    public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
      this.parameterMetaObject = parameterMetaObject;
      this.fallbackParameterObject = fallbackParameterObject; }}Copy the code

MetaObject is the metadata of an object instance, which contains the attributes of the object, the attribute set/ GET method and the instance. It can be used to determine whether the instance has a GET /set method of an attribute and initiate the call. When executing the statement, if the parameter passed to us is non-empty and non-map, the MetaObject of the passed parameter will be parsed and given to parameterMetaObject. Then ogNL will call the method provided by MetaObject to call the get method of the corresponding attribute.

The fallbackParameterObject is whether TypeHandler exists for the type of the parameter we pass in.

ContextMap (ContextMap) : ContextMap (ContextMap) : ContextMap (ContextMap) : ContextMap (ContextMap) : ContextMap

  1. ContextMap is put back if the specified attribute exists, but only additional elements added by Bind will exist in the Map.
  2. ParameterMetaObject is generated when the parameter passed in the query is an object. MetaObject’s getValue method is used in this case. MetaObject’s getValue method looks for the property in the parameter and calls to get the value.
  3. ParameterMetaObject does not have a get method, so it returns the parameter value.
static class ContextMap extends HashMap<String, Object> {@override public Object get(Object key) {String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey); } // parameterMetaObject returns null if (parameterMetaObject == null) {return null; } // If (fallbackParameterObject &&!!) if (fallbackParameterObject &&!! parameterMetaObject.hasGetter(strKey)) { return parameterMetaObject.getOriginalObject(); } else {/ / get the return from parameter in the corresponding properties of the get method parameterMetaObject. GetValue (strKey); }}}Copy the code

ParameterMetaObject is not created when the parameter is a Map. Therefore, it is not possible to obtain the property value. This problem is solved with PropertyAccessor, an interface defined in Ognl that specifies the specific behavior when getting and setting properties. Mybatis defines an implementation of ContextMap PropertyAccessor, ContextAccessor. ContextMap (ContextMap); ContextMap (ContextMap); ContextMap (ContextMap); ContextMap (ContextMap) If it is Map, its GET method is called.

static class ContextAccessor implements PropertyAccessor {
    @Override
    public Object getProperty(Map context, Object target, Object name) {
      Map map = (Map) target;
      Object result = map.get(name);
      if(map.containsKey(name) || result ! =null) {
        return result;
      }
	  // PARAMETER_OBJECT_KEY is the specified parameter
      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
      if (parameterObject instanceof Map) {
        return ((Map)parameterObject).get(name);
      }

      return null; }}Copy the code

Each sqlSession call generates a ContextMap for the current call to be used by this call.

How to replace

Here’s a quick recap from the previous article on how XML is transformed into executable SQL.

In the XML configuration parsing phase, MyBatis will parse each statement using XML configuration into an MappedStatement, which encapsulates each attribute of the statement, such as timeout, resultType, fetchSize, etc. In addition to these attributes is the SqlSource attribute, which encapsulates the XML of the statement and is provided in the executable SQL.

In XML parsing, each XML node is parsed to its corresponding SqlNode type, and organized into a tree structure according to the configuration, and then the root node is saved to the SqlSource. When SqlSession is used to execute statements, SQL will be obtained from SqlSource. When SQL is generated, THE SqlSource will traverse each node in the tree with depth-first strategy according to the conditions and call the apply method of the node, so that the node will add its own statements to the final SQL.

For example, the queryUserById statement above is resolved to the following structure:

TextSqlNode replaces ${} with TextSqlNode’s applay method when it generates its own SQL.

The TextSqlNode applay method gets all the ${} references in the text and replaces them one by one. During the substitution, Ognl is used to parse the expression in ${} to obtain the valueOf the specified attribute from ContextMap. If the value obtained is not null, string.valueof is used to obtain the String of the value and replace it; if it is null, an empty String is used to replace it.

public class TextSqlNode implements SqlNode { private static class BindingTokenParser implements TokenHandler { @override public String handleToken(String Content) {// ContextMap Object parameter = that contains the specified parameters context.getBindings().get("_parameter"); if (parameter == null) { context.getBindings().put("value", null); } else if (SimpleTypeRegistry isSimpleType (parameter getClass ())) {/ / a simple type is "value" is added to the context, the additional The values are context.getBindings().put("value", parameter); Object value = ogNLCache.getValue (content, context.getBindings()); String srtValue = value == null? "" : String.valueOf(value); This plugin is not open yet. Default is null checkInjection(srtValue); return srtValue; }}}Copy the code

As you can see, TextSqlNode also adds our argument to the context with a key of ‘value’ if we give it a simple type.

Simple types have the following:

  static {
    SIMPLE_TYPE_SET.add(String.class);
    SIMPLE_TYPE_SET.add(Byte.class);
    SIMPLE_TYPE_SET.add(Short.class);
    SIMPLE_TYPE_SET.add(Character.class);
    SIMPLE_TYPE_SET.add(Integer.class);
    SIMPLE_TYPE_SET.add(Long.class);
    SIMPLE_TYPE_SET.add(Float.class);
    SIMPLE_TYPE_SET.add(Double.class);
    SIMPLE_TYPE_SET.add(Boolean.class);
    SIMPLE_TYPE_SET.add(Date.class);
    SIMPLE_TYPE_SET.add(Class.class);
    SIMPLE_TYPE_SET.add(BigInteger.class);
    SIMPLE_TYPE_SET.add(BigDecimal.class);
  }
  
Copy the code