org.njqspringboot.support.mybatis.locker.interceptor.OptimisticLocker.java Source code

Java tutorial

Introduction

Here is the source code for org.njqspringboot.support.mybatis.locker.interceptor.OptimisticLocker.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 342252328@qq.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.njqspringboot.support.mybatis.locker.interceptor;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import org.apache.ibatis.type.TypeHandler;
import org.njqspringboot.support.mybatis.locker.annotation.VersionLocker;
import org.njqspringboot.support.mybatis.locker.cache.Cache;
import org.njqspringboot.support.mybatis.locker.cache.Cache.MethodSignature;
import org.njqspringboot.support.mybatis.locker.cache.LocalVersionLockerCache;
import org.njqspringboot.support.mybatis.locker.cache.VersionLockerCache;
import org.njqspringboot.support.mybatis.locker.util.PluginUtil;

/**
 * <p>MyBatis???<br>
 * <p>MyBatis Optimistic Locker Plugin<br>
 * 
 * @author 342252328@qq.com
 * @date 2016-05-27
 * @version 1.0
 * @since JDK1.7
 *
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }),
        @Signature(type = ParameterHandler.class, method = "setParameters", args = { PreparedStatement.class }) })
public class OptimisticLocker implements Interceptor {

    private static final Log log = LogFactory.getLog(OptimisticLocker.class);

    private static VersionLocker trueLocker;
    static {
        try {
            trueLocker = OptimisticLocker.class.getDeclaredMethod("versionValue")
                    .getAnnotation(VersionLocker.class);
        } catch (NoSuchMethodException | SecurityException e) {
            throw new RuntimeException("The plugin init faild.", e);
        }
    }

    private Properties props = null;
    private VersionLockerCache versionLockerCache = new LocalVersionLockerCache();
    Map<String, Class<?>> mapperMap = null;

    @VersionLocker(true)
    private void versionValue() {
    }

    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Object intercept(Invocation invocation) throws Exception {

        String versionColumn;
        if (null == props || props.isEmpty()) {
            versionColumn = "version";
        } else {
            versionColumn = props.getProperty("versionColumn", "version");
        }

        String interceptMethod = invocation.getMethod().getName();
        if ("prepare".equals(interceptMethod)) {

            StatementHandler handler = (StatementHandler) PluginUtil.processTarget(invocation.getTarget());
            MetaObject hm = SystemMetaObject.forObject(handler);

            MappedStatement ms = (MappedStatement) hm.getValue("delegate.mappedStatement");
            SqlCommandType sqlCmdType = ms.getSqlCommandType();
            if (sqlCmdType != SqlCommandType.UPDATE) {
                return invocation.proceed();
            }

            BoundSql boundSql = (BoundSql) hm.getValue("delegate.boundSql");

            VersionLocker vl = getVersionLocker(ms, boundSql);
            if (null != vl && !vl.value()) {
                return invocation.proceed();
            }

            Object originalVersion = hm.getValue("delegate.boundSql.parameterObject." + versionColumn);
            Object versionIncr = castTypeAndOptValue(originalVersion,
                    hm.getValue("delegate.boundSql.parameterObject"), ValueType.INCREASE);
            hm.setValue("delegate.boundSql.parameterObject." + versionColumn, versionIncr);

            String originalSql = (String) hm.getValue("delegate.boundSql.sql");
            StringBuilder builder = new StringBuilder(originalSql);
            builder.append(" and ");
            builder.append(versionColumn);
            builder.append(" = ?");
            hm.setValue("delegate.boundSql.sql", builder.toString());

            if (log.isDebugEnabled()) {
                log.debug("==> originalSql: " + originalSql);
            }

            return invocation.proceed();

        } else if ("setParameters".equals(interceptMethod)) {

            ParameterHandler handler = (ParameterHandler) PluginUtil.processTarget(invocation.getTarget());
            MetaObject hm = SystemMetaObject.forObject(handler);

            MappedStatement ms = (MappedStatement) hm.getValue("mappedStatement");
            SqlCommandType sqlCmdType = ms.getSqlCommandType();
            if (sqlCmdType != SqlCommandType.UPDATE) {
                return invocation.proceed();
            }

            Configuration configuration = ms.getConfiguration();
            BoundSql boundSql = (BoundSql) hm.getValue("boundSql");

            VersionLocker vl = getVersionLocker(ms, boundSql);
            if (null != vl && !vl.value()) {
                return invocation.proceed();
            }

            Object result = invocation.proceed();

            ParameterMapping versionMapping = new ParameterMapping.Builder(configuration, versionColumn,
                    Object.class).build();

            Object parameterObject = boundSql.getParameterObject();

            MetaObject pm = configuration.newMetaObject(parameterObject);
            if (parameterObject instanceof MapperMethod.ParamMap<?>) {
                MapperMethod.ParamMap<?> paramMap = (MapperMethod.ParamMap<?>) parameterObject;
                if (!paramMap.containsKey(versionColumn)) {
                    throw new TypeException("All the base type parameters must add MyBatis's @Param Annotaion");
                }
            }
            Object value = pm.getValue(versionColumn);
            TypeHandler typeHandler = versionMapping.getTypeHandler();
            JdbcType jdbcType = versionMapping.getJdbcType();

            if (value == null && jdbcType == null) {
                jdbcType = configuration.getJdbcTypeForNull();
            }
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            try {
                PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
                Object val = castTypeAndOptValue(value, parameterObject, ValueType.DECREASE);
                typeHandler.setParameter(ps, parameterMappings.size() + 1, val, jdbcType);
            } catch (TypeException e) {
                throw new TypeException(
                        "Could not set parameters for mapping: " + parameterMappings + ". Cause: " + e, e);
            } catch (SQLException e) {
                throw new TypeException(
                        "Could not set parameters for mapping: " + parameterMappings + ". Cause: " + e, e);
            }
            return result;
        }
        return invocation.proceed();
    }

    private Object castTypeAndOptValue(Object value, Object parameterObject, ValueType vt) {
        Class<?> valType = value.getClass();
        if (valType == Long.class || valType == long.class) {
            return (Long) value + vt.value;
        } else if (valType == Integer.class || valType == int.class) {
            return (Integer) value + vt.value;
        } else if (valType == Float.class || valType == float.class) {
            return (Float) value + vt.value;
        } else if (valType == Double.class || valType == double.class) {
            return (Double) value + vt.value;
        } else {
            if (parameterObject instanceof MapperMethod.ParamMap<?>) {
                throw new TypeException("All the base type parameters must add MyBatis's @Param Annotaion");
            } else {
                throw new TypeException("Property 'version' in " + parameterObject.getClass().getSimpleName()
                        + " must be [ long, int, float, double ] or [ Long, Integer, Float, Double ]");
            }
        }
    }

    private VersionLocker getVersionLocker(MappedStatement ms, BoundSql boundSql) {

        Class<?>[] paramCls = null;
        Object paramObj = boundSql.getParameterObject();

        /******************?????3?***********************/
        /******************Process param must order by below ***********************/
        // 1??@Param?
        // 1?Process @Param param
        if (paramObj instanceof MapperMethod.ParamMap<?>) {
            MapperMethod.ParamMap<?> mmp = (MapperMethod.ParamMap<?>) paramObj;
            if (null != mmp && !mmp.isEmpty()) {
                paramCls = new Class<?>[mmp.size() / 2];
                int mmpLen = mmp.size() / 2;
                for (int i = 0; i < mmpLen; i++) {
                    Object index = mmp.get("param" + (i + 1));
                    paramCls[i] = index.getClass();
                }
            }

            // 2??Map?
            // 2?Process Map param
        } else if (paramObj instanceof Map) {
            paramCls = new Class<?>[] { Map.class };

            // 3??POJO?
            // 3?Process POJO entity param
        } else {
            paramCls = new Class<?>[] { paramObj.getClass() };
        }

        String id = ms.getId();
        Cache.MethodSignature vm = new MethodSignature(id, paramCls);
        VersionLocker versionLocker = versionLockerCache.getVersionLocker(vm);
        if (null != versionLocker) {
            return versionLocker;
        }

        // synchronized???
        // Here remove synchronized keyword or ReentrantLock, because it's a idempotent operation
        if (null == mapperMap || mapperMap.isEmpty()) {
            mapperMap = new HashMap<>();
            Collection<Class<?>> mappers = ms.getConfiguration().getMapperRegistry().getMappers();
            if (null != mappers && !mappers.isEmpty()) {
                for (Class<?> me : mappers) {
                    mapperMap.put(me.getName(), me);
                }
            }
        }

        int pos = id.lastIndexOf(".");
        String nameSpace = id.substring(0, pos);
        if (mapperMap.containsKey(nameSpace)) {
            Class<?> mapper = mapperMap.get(nameSpace);
            Method m;
            try {
                m = mapper.getMethod(id.substring(pos + 1), paramCls);
            } catch (NoSuchMethodException | SecurityException e) {
                throw new RuntimeException("The Map type param error." + e, e);
            }
            versionLocker = m.getAnnotation(VersionLocker.class);
            if (null == versionLocker) {
                versionLocker = trueLocker;
            }
            if (!versionLockerCache.containMethodSignature(vm)) {
                versionLockerCache.cacheMethod(vm, versionLocker);
            }
            return versionLocker;
        } else {
            throw new RuntimeException("Config info error, maybe you have not config the Mapper interface");
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler || target instanceof ParameterHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        if (null != properties && !properties.isEmpty())
            props = properties;
    }

    private enum ValueType {
        INCREASE(1), DECREASE(-1);

        private Integer value;

        private ValueType(Integer value) {
            this.value = value;
        }
    }

}