001/*
002 *    Copyright 2009-2023 the original author or authors.
003 *
004 *    Licensed under the Apache License, Version 2.0 (the "License");
005 *    you may not use this file except in compliance with the License.
006 *    You may obtain a copy of the License at
007 *
008 *       https://www.apache.org/licenses/LICENSE-2.0
009 *
010 *    Unless required by applicable law or agreed to in writing, software
011 *    distributed under the License is distributed on an "AS IS" BASIS,
012 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *    See the License for the specific language governing permissions and
014 *    limitations under the License.
015 */
016package com.mybatisflex.core.mybatis.binding;
017
018import com.mybatisflex.core.util.MapUtil;
019import org.apache.ibatis.binding.MapperMethod;
020import org.apache.ibatis.reflection.ExceptionUtil;
021import org.apache.ibatis.session.SqlSession;
022
023import java.io.Serializable;
024import java.lang.invoke.MethodHandle;
025import java.lang.invoke.MethodHandles;
026import java.lang.invoke.MethodHandles.Lookup;
027import java.lang.invoke.MethodType;
028import java.lang.reflect.Constructor;
029import java.lang.reflect.InvocationHandler;
030import java.lang.reflect.InvocationTargetException;
031import java.lang.reflect.Method;
032import java.util.Map;
033
034/**
035 * @author Clinton Begin
036 * @author Eduardo Macarron
037 * @author Michael Yang
038 * <p>
039 * 参考 MapperProxy<T> 并开放 MapperMethodInvoker,方便子类代理
040 */
041public class MybatisMapperProxy<T> implements InvocationHandler, Serializable {
042
043    private static final long serialVersionUID = -4724728412955527868L;
044    private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
045        | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
046    private static final Constructor<Lookup> lookupConstructor;
047    private static final Method privateLookupInMethod;
048    protected final SqlSession sqlSession;
049    private final Class<T> mapperInterface;
050    private final Map<Method, MapperMethodInvoker> methodCache;
051
052    public MybatisMapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
053        this.sqlSession = sqlSession;
054        this.mapperInterface = mapperInterface;
055        this.methodCache = methodCache;
056    }
057
058    static {
059        Method privateLookupIn;
060        try {
061            privateLookupIn = MethodHandles.class.getMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
062        } catch (NoSuchMethodException e) {
063            privateLookupIn = null;
064        }
065        privateLookupInMethod = privateLookupIn;
066
067        Constructor<Lookup> lookup = null;
068        if (privateLookupInMethod == null) {
069            // JDK 1.8
070            try {
071                lookup = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
072                lookup.setAccessible(true);
073            } catch (NoSuchMethodException e) {
074                throw new IllegalStateException(
075                    "There is neither 'privateLookupIn(Class, Lookup)' nor 'Lookup(Class, int)' method in java.lang.invoke.MethodHandles.",
076                    e);
077            } catch (Exception e) {
078                lookup = null;
079            }
080        }
081        lookupConstructor = lookup;
082    }
083
084    @Override
085    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
086        try {
087            if (Object.class.equals(method.getDeclaringClass())) {
088                return method.invoke(this, args);
089            }
090            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
091        } catch (Throwable t) {
092            throw ExceptionUtil.unwrapThrowable(t);
093        }
094    }
095
096
097    protected MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
098        try {
099            return MapUtil.computeIfAbsent(methodCache, method, m -> {
100                if (!m.isDefault()) {
101                    return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
102                }
103                try {
104                    if (privateLookupInMethod == null) {
105                        return new DefaultMethodInvoker(getMethodHandleJava8(method));
106                    }
107                    return new DefaultMethodInvoker(getMethodHandleJava9(method));
108                } catch (IllegalAccessException | InstantiationException | InvocationTargetException
109                         | NoSuchMethodException e) {
110                    throw new RuntimeException(e);
111                }
112            });
113        } catch (RuntimeException re) {
114            Throwable cause = re.getCause();
115            throw cause == null ? re : cause;
116        }
117    }
118
119    private MethodHandle getMethodHandleJava9(Method method)
120        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
121        final Class<?> declaringClass = method.getDeclaringClass();
122        return ((Lookup) privateLookupInMethod.invoke(null, declaringClass, MethodHandles.lookup())).findSpecial(
123            declaringClass, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
124            declaringClass);
125    }
126
127    private MethodHandle getMethodHandleJava8(Method method)
128        throws IllegalAccessException, InstantiationException, InvocationTargetException {
129        final Class<?> declaringClass = method.getDeclaringClass();
130        return lookupConstructor.newInstance(declaringClass, ALLOWED_MODES).unreflectSpecial(method, declaringClass);
131    }
132
133    public interface MapperMethodInvoker {
134        Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;
135    }
136
137    private static class PlainMethodInvoker implements MapperMethodInvoker {
138        private final MapperMethod mapperMethod;
139
140        public PlainMethodInvoker(MapperMethod mapperMethod) {
141            this.mapperMethod = mapperMethod;
142        }
143
144        @Override
145        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
146            return mapperMethod.execute(sqlSession, args);
147        }
148    }
149
150    private static class DefaultMethodInvoker implements MapperMethodInvoker {
151        private final MethodHandle methodHandle;
152
153        public DefaultMethodInvoker(MethodHandle methodHandle) {
154            this.methodHandle = methodHandle;
155        }
156
157        @Override
158        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
159            return methodHandle.bindTo(proxy).invokeWithArguments(args);
160        }
161    }
162}