| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.dubbo.rpc.support; |
| |
| import org.apache.dubbo.common.URL; |
| import org.apache.dubbo.common.extension.ExtensionDirector; |
| import org.apache.dubbo.common.extension.ExtensionInjector; |
| import org.apache.dubbo.common.utils.ArrayUtils; |
| import org.apache.dubbo.common.utils.ConfigUtils; |
| import org.apache.dubbo.common.utils.JsonUtils; |
| import org.apache.dubbo.common.utils.PojoUtils; |
| import org.apache.dubbo.common.utils.ReflectUtils; |
| import org.apache.dubbo.common.utils.StringUtils; |
| import org.apache.dubbo.rpc.AsyncRpcResult; |
| import org.apache.dubbo.rpc.Invocation; |
| import org.apache.dubbo.rpc.Invoker; |
| import org.apache.dubbo.rpc.ProxyFactory; |
| import org.apache.dubbo.rpc.Result; |
| import org.apache.dubbo.rpc.RpcException; |
| import org.apache.dubbo.rpc.RpcInvocation; |
| |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.Type; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import static org.apache.dubbo.rpc.Constants.FAIL_PREFIX; |
| import static org.apache.dubbo.rpc.Constants.FORCE_PREFIX; |
| import static org.apache.dubbo.rpc.Constants.MOCK_KEY; |
| import static org.apache.dubbo.rpc.Constants.RETURN_KEY; |
| import static org.apache.dubbo.rpc.Constants.RETURN_PREFIX; |
| import static org.apache.dubbo.rpc.Constants.THROW_PREFIX; |
| |
| public final class MockInvoker<T> implements Invoker<T> { |
| private final ProxyFactory proxyFactory; |
| private static final Map<String, Invoker<?>> MOCK_MAP = new ConcurrentHashMap<>(); |
| private static final Map<String, Throwable> THROWABLE_MAP = new ConcurrentHashMap<>(); |
| |
| private final URL url; |
| private final Class<T> type; |
| |
| public MockInvoker(URL url, Class<T> type) { |
| this.url = url; |
| this.type = type; |
| this.proxyFactory = url.getOrDefaultFrameworkModel() |
| .getExtensionLoader(ProxyFactory.class) |
| .getAdaptiveExtension(); |
| } |
| |
| public static Object parseMockValue(String mock) throws Exception { |
| return parseMockValue(mock, null); |
| } |
| |
| public static Object parseMockValue(String mock, Type[] returnTypes) throws Exception { |
| Object value; |
| if ("empty".equals(mock)) { |
| value = ReflectUtils.getEmptyObject( |
| returnTypes != null && returnTypes.length > 0 ? (Class<?>) returnTypes[0] : null); |
| } else if ("null".equals(mock)) { |
| value = null; |
| } else if ("true".equals(mock)) { |
| value = true; |
| } else if ("false".equals(mock)) { |
| value = false; |
| } else if (mock.length() >= 2 |
| && (mock.startsWith("\"") && mock.endsWith("\"") || mock.startsWith("\'") && mock.endsWith("\'"))) { |
| value = mock.subSequence(1, mock.length() - 1); |
| } else if (returnTypes != null && returnTypes.length > 0 && returnTypes[0] == String.class) { |
| value = mock; |
| } else if (StringUtils.isNumeric(mock, false)) { |
| value = JsonUtils.toJavaObject(mock, Object.class); |
| } else if (mock.startsWith("{")) { |
| value = JsonUtils.toJavaObject(mock, Map.class); |
| } else if (mock.startsWith("[")) { |
| value = JsonUtils.toJavaList(mock, Object.class); |
| } else { |
| value = mock; |
| } |
| if (ArrayUtils.isNotEmpty(returnTypes)) { |
| value = PojoUtils.realize(value, (Class<?>) returnTypes[0], returnTypes.length > 1 ? returnTypes[1] : null); |
| } |
| return value; |
| } |
| |
| @Override |
| public Result invoke(Invocation invocation) throws RpcException { |
| if (invocation instanceof RpcInvocation) { |
| ((RpcInvocation) invocation).setInvoker(this); |
| } |
| String mock = getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY); |
| |
| if (StringUtils.isBlank(mock)) { |
| throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url)); |
| } |
| mock = normalizeMock(URL.decode(mock)); |
| if (mock.startsWith(RETURN_PREFIX)) { |
| mock = mock.substring(RETURN_PREFIX.length()).trim(); |
| try { |
| Type[] returnTypes = RpcUtils.getReturnTypes(invocation); |
| Object value = parseMockValue(mock, returnTypes); |
| return AsyncRpcResult.newDefaultAsyncResult(value, invocation); |
| } catch (Exception ew) { |
| throw new RpcException( |
| "mock return invoke error. method :" + invocation.getMethodName() + ", mock:" + mock + ", url: " |
| + url, |
| ew); |
| } |
| } else if (mock.startsWith(THROW_PREFIX)) { |
| mock = mock.substring(THROW_PREFIX.length()).trim(); |
| if (StringUtils.isBlank(mock)) { |
| throw new RpcException("mocked exception for service degradation."); |
| } else { // user customized class |
| Throwable t = getThrowable(mock); |
| throw new RpcException(RpcException.BIZ_EXCEPTION, t); |
| } |
| } else { // impl mock |
| try { |
| Invoker<T> invoker = getInvoker(mock); |
| return invoker.invoke(invocation); |
| } catch (Throwable t) { |
| throw new RpcException("Failed to create mock implementation class " + mock, t); |
| } |
| } |
| } |
| |
| public static Throwable getThrowable(String throwstr) { |
| Throwable throwable = THROWABLE_MAP.get(throwstr); |
| if (throwable != null) { |
| return throwable; |
| } |
| |
| try { |
| Throwable t; |
| Class<?> bizException = ReflectUtils.forName(throwstr); |
| Constructor<?> constructor; |
| constructor = ReflectUtils.findConstructor(bizException, String.class); |
| t = (Throwable) constructor.newInstance(new Object[] {"mocked exception for service degradation."}); |
| if (THROWABLE_MAP.size() < 1000) { |
| THROWABLE_MAP.put(throwstr, t); |
| } |
| return t; |
| } catch (Exception e) { |
| throw new RpcException("mock throw error :" + throwstr + " argument error.", e); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Invoker<T> getInvoker(String mock) { |
| Class<T> serviceType = (Class<T>) ReflectUtils.forName(url.getServiceInterface()); |
| String mockService = ConfigUtils.isDefault(mock) ? serviceType.getName() + "Mock" : mock; |
| Invoker<T> invoker = (Invoker<T>) MOCK_MAP.get(mockService); |
| if (invoker != null) { |
| return invoker; |
| } |
| |
| T mockObject = (T) getMockObject(url.getOrDefaultApplicationModel().getExtensionDirector(), mock, serviceType); |
| invoker = proxyFactory.getInvoker(mockObject, serviceType, url); |
| if (MOCK_MAP.size() < 10000) { |
| MOCK_MAP.put(mockService, invoker); |
| } |
| return invoker; |
| } |
| |
| @SuppressWarnings("unchecked") |
| public static Object getMockObject(ExtensionDirector extensionDirector, String mockService, Class serviceType) { |
| boolean isDefault = ConfigUtils.isDefault(mockService); |
| if (isDefault) { |
| mockService = serviceType.getName() + "Mock"; |
| } |
| |
| Class<?> mockClass; |
| try { |
| mockClass = ReflectUtils.forName(mockService); |
| } catch (Exception e) { |
| if (!isDefault) { // does not check Spring bean if it is default config. |
| ExtensionInjector extensionFactory = extensionDirector |
| .getExtensionLoader(ExtensionInjector.class) |
| .getAdaptiveExtension(); |
| Object obj = extensionFactory.getInstance(serviceType, mockService); |
| if (obj != null) { |
| return obj; |
| } |
| } |
| throw new IllegalStateException( |
| "Did not find mock class or instance " |
| + mockService |
| + ", please check if there's mock class or instance implementing interface " |
| + serviceType.getName(), |
| e); |
| } |
| if (mockClass == null || !serviceType.isAssignableFrom(mockClass)) { |
| throw new IllegalStateException( |
| "The mock class " + mockClass.getName() + " not implement interface " + serviceType.getName()); |
| } |
| |
| try { |
| return mockClass.newInstance(); |
| } catch (InstantiationException e) { |
| throw new IllegalStateException("No default constructor from mock class " + mockClass.getName(), e); |
| } catch (IllegalAccessException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| |
| /** |
| * Normalize mock string: |
| * |
| * <ol> |
| * <li>return => return null</li> |
| * <li>fail => default</li> |
| * <li>force => default</li> |
| * <li>fail:throw/return foo => throw/return foo</li> |
| * <li>force:throw/return foo => throw/return foo</li> |
| * </ol> |
| * |
| * @param mock mock string |
| * @return normalized mock string |
| */ |
| public static String normalizeMock(String mock) { |
| if (mock == null) { |
| return mock; |
| } |
| |
| mock = mock.trim(); |
| |
| if (mock.length() == 0) { |
| return mock; |
| } |
| |
| if (RETURN_KEY.equalsIgnoreCase(mock)) { |
| return RETURN_PREFIX + "null"; |
| } |
| |
| if (ConfigUtils.isDefault(mock) || "fail".equalsIgnoreCase(mock) || "force".equalsIgnoreCase(mock)) { |
| return "default"; |
| } |
| |
| if (mock.startsWith(FAIL_PREFIX)) { |
| mock = mock.substring(FAIL_PREFIX.length()).trim(); |
| } |
| |
| if (mock.startsWith(FORCE_PREFIX)) { |
| mock = mock.substring(FORCE_PREFIX.length()).trim(); |
| } |
| |
| if (mock.startsWith(RETURN_PREFIX) || mock.startsWith(THROW_PREFIX)) { |
| mock = mock.replace('`', '"'); |
| } |
| |
| return mock; |
| } |
| |
| @Override |
| public URL getUrl() { |
| return this.url; |
| } |
| |
| @Override |
| public boolean isAvailable() { |
| return true; |
| } |
| |
| @Override |
| public void destroy() { |
| // do nothing |
| } |
| |
| @Override |
| public Class<T> getInterface() { |
| return type; |
| } |
| } |