From 1819b77d4171d258a1a55d403ed7adbb8e1ac816 Mon Sep 17 00:00:00 2001 From: zhuyong Date: Tue, 15 May 2018 14:38:27 +0800 Subject: [PATCH] support generic invoke for http protocol #1768 --- .../dubbo/rpc/filter/GenericImplFilter.java | 8 +- dubbo-rpc/dubbo-rpc-http/pom.xml | 6 + .../dubbo/rpc/protocol/http/HttpProtocol.java | 112 +++++++++- .../rpc/protocol/http/HttpProtocolTest.java | 198 ++++++++++++++++++ .../dubbo/rpc/protocol/http/HttpService.java | 33 +++ .../rpc/protocol/http/HttpServiceImpl.java | 64 ++++++ 6 files changed, 415 insertions(+), 6 deletions(-) create mode 100644 dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpProtocolTest.java create mode 100644 dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpService.java create mode 100644 dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpServiceImpl.java diff --git a/dubbo-rpc/dubbo-rpc-api/src/main/java/com/alibaba/dubbo/rpc/filter/GenericImplFilter.java b/dubbo-rpc/dubbo-rpc-api/src/main/java/com/alibaba/dubbo/rpc/filter/GenericImplFilter.java index 99bced54e74..6be08fef47c 100644 --- a/dubbo-rpc/dubbo-rpc-api/src/main/java/com/alibaba/dubbo/rpc/filter/GenericImplFilter.java +++ b/dubbo-rpc/dubbo-rpc-api/src/main/java/com/alibaba/dubbo/rpc/filter/GenericImplFilter.java @@ -155,13 +155,13 @@ public Result invoke(Invoker invoker, Invocation invocation) throws RpcExcept for (Object arg : args) { if (!(byte[].class == arg.getClass())) { - error(byte[].class.getName(), arg.getClass().getName()); + error(generic, byte[].class.getName(), arg.getClass().getName()); } } } else if (ProtocolUtils.isBeanGenericSerialization(generic)) { for (Object arg : args) { if (!(arg instanceof JavaBeanDescriptor)) { - error(JavaBeanDescriptor.class.getName(), arg.getClass().getName()); + error(generic, JavaBeanDescriptor.class.getName(), arg.getClass().getName()); } } } @@ -172,10 +172,10 @@ public Result invoke(Invoker invoker, Invocation invocation) throws RpcExcept return invoker.invoke(invocation); } - private void error(String expected, String actual) throws RpcException { + private void error(String generic, String expected, String actual) throws RpcException { throw new RpcException( "Generic serialization [" + - Constants.GENERIC_SERIALIZATION_NATIVE_JAVA + + generic + "] only support message type " + expected + " and your message type is " + diff --git a/dubbo-rpc/dubbo-rpc-http/pom.xml b/dubbo-rpc/dubbo-rpc-http/pom.xml index 427b70ba6a3..93d8a0f028f 100644 --- a/dubbo-rpc/dubbo-rpc-http/pom.xml +++ b/dubbo-rpc/dubbo-rpc-http/pom.xml @@ -48,5 +48,11 @@ org.springframework spring-web + + com.alibaba + dubbo-serialization-jdk + ${project.parent.version} + test + \ No newline at end of file diff --git a/dubbo-rpc/dubbo-rpc-http/src/main/java/com/alibaba/dubbo/rpc/protocol/http/HttpProtocol.java b/dubbo-rpc/dubbo-rpc-http/src/main/java/com/alibaba/dubbo/rpc/protocol/http/HttpProtocol.java index 27a59134e8c..22f821ca143 100644 --- a/dubbo-rpc/dubbo-rpc-http/src/main/java/com/alibaba/dubbo/rpc/protocol/http/HttpProtocol.java +++ b/dubbo-rpc/dubbo-rpc-http/src/main/java/com/alibaba/dubbo/rpc/protocol/http/HttpProtocol.java @@ -18,6 +18,16 @@ import com.alibaba.dubbo.common.Constants; import com.alibaba.dubbo.common.URL; +import com.alibaba.dubbo.common.beanutil.JavaBeanAccessor; +import com.alibaba.dubbo.common.beanutil.JavaBeanDescriptor; +import com.alibaba.dubbo.common.beanutil.JavaBeanSerializeUtil; +import com.alibaba.dubbo.common.extension.ExtensionLoader; +import com.alibaba.dubbo.common.io.UnsafeByteArrayInputStream; +import com.alibaba.dubbo.common.io.UnsafeByteArrayOutputStream; +import com.alibaba.dubbo.common.serialize.Serialization; +import com.alibaba.dubbo.common.utils.PojoUtils; +import com.alibaba.dubbo.common.utils.ReflectUtils; +import com.alibaba.dubbo.common.utils.StringUtils; import com.alibaba.dubbo.remoting.http.HttpBinder; import com.alibaba.dubbo.remoting.http.HttpHandler; import com.alibaba.dubbo.remoting.http.HttpServer; @@ -25,16 +35,21 @@ import com.alibaba.dubbo.rpc.RpcException; import com.alibaba.dubbo.rpc.protocol.AbstractProxyProtocol; +import com.alibaba.dubbo.rpc.support.ProtocolUtils; +import org.aopalliance.intercept.MethodInvocation; import org.springframework.remoting.RemoteAccessException; import org.springframework.remoting.httpinvoker.HttpComponentsHttpInvokerRequestExecutor; import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean; import org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter; import org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor; +import org.springframework.remoting.support.RemoteInvocation; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.util.Map; @@ -74,7 +89,89 @@ protected Runnable doExport(final T impl, Class type, URL url) throws Rpc server = httpBinder.bind(url, new InternalHandler()); serverMap.put(addr, server); } - final HttpInvokerServiceExporter httpServiceExporter = new HttpInvokerServiceExporter(); + final HttpInvokerServiceExporter httpServiceExporter = new HttpInvokerServiceExporter() { + @Override + protected Object invoke(RemoteInvocation invocation, Object targetObject) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + if (invocation.getMethodName().equals(Constants.$INVOKE) + && invocation.getArguments() != null + && invocation.getArguments().length == 3) { + String name = ((String) invocation.getArguments()[0]).trim(); + String[] types = (String[]) invocation.getArguments()[1]; + Object[] args = (Object[]) invocation.getArguments()[2]; + + Class[] params; + try { + Method method = ReflectUtils.findMethodByMethodSignature(this.getServiceInterface(), name, types); + params = method.getParameterTypes(); + if (args == null) { + args = new Object[params.length]; + } + + String generic = (String) invocation.getAttribute(Constants.GENERIC_KEY); + if (StringUtils.isEmpty(generic) + || ProtocolUtils.isDefaultGenericSerialization(generic)) { + args = PojoUtils.realize(args, params, method.getGenericParameterTypes()); + } else if (ProtocolUtils.isJavaGenericSerialization(generic)) { + for (int i = 0; i < args.length; i++) { + if (byte[].class == args[i].getClass()) { + try { + UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i]); + args[i] = ExtensionLoader.getExtensionLoader(Serialization.class) + .getExtension(Constants.GENERIC_SERIALIZATION_NATIVE_JAVA) + .deserialize(null, is).readObject(); + } catch (Exception e) { + throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e); + } + } else { + throw new RpcException( + "Generic serialization [" + generic + "] only support message type " + + byte[].class + " and your message type is " + + args[i].getClass()); + } + } + } else if (ProtocolUtils.isBeanGenericSerialization(generic)) { + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof JavaBeanDescriptor) { + args[i] = JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]); + } else { + throw new RpcException( + "Generic serialization [" + generic + "] only support message type " + + JavaBeanDescriptor.class.getName() + " and your message type is " + + args[i].getClass().getName()); + } + } + } + + RemoteInvocation invocation2 = invocation; + invocation2.setMethodName(name); + invocation2.setParameterTypes(params); + invocation2.setArguments(args); + + Object result = super.invoke(invocation, targetObject); + if (ProtocolUtils.isJavaGenericSerialization(generic)) { + try { + UnsafeByteArrayOutputStream os = new UnsafeByteArrayOutputStream(512); + ExtensionLoader.getExtensionLoader(Serialization.class) + .getExtension(Constants.GENERIC_SERIALIZATION_NATIVE_JAVA) + .serialize(null, os).writeObject(result); + return os.toByteArray(); + } catch (IOException e) { + throw new RpcException("Serialize result failed.", e); + } + } else if (ProtocolUtils.isBeanGenericSerialization(generic)) { + return JavaBeanSerializeUtil.serialize(result, JavaBeanAccessor.METHOD); + } else { + return PojoUtils.generalize(result); + } + } catch (NoSuchMethodException e) { + throw new RpcException(e); + } catch (Exception e) { + throw new RpcException(e); + } + } + return super.invoke(invocation, targetObject); + } + }; httpServiceExporter.setServiceInterface(type); httpServiceExporter.setService(impl); try { @@ -95,7 +192,18 @@ public void run() { @Override @SuppressWarnings("unchecked") protected T doRefer(final Class serviceType, final URL url) throws RpcException { - final HttpInvokerProxyFactoryBean httpProxyFactoryBean = new HttpInvokerProxyFactoryBean(); + final HttpInvokerProxyFactoryBean httpProxyFactoryBean = new HttpInvokerProxyFactoryBean() { + @Override + protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { + RemoteInvocation result = super.createRemoteInvocation(methodInvocation); + for (Map.Entry entry : url.getParameters().entrySet()) { + if (!StringUtils.isBlank(entry.getValue())) { + result.addAttribute(entry.getKey(), entry.getValue()); + } + } + return result; + } + }; httpProxyFactoryBean.setServiceUrl(url.toIdentityString()); httpProxyFactoryBean.setServiceInterface(serviceType); String client = url.getParameter(Constants.CLIENT_KEY); diff --git a/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpProtocolTest.java b/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpProtocolTest.java new file mode 100644 index 00000000000..b879a9490bb --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpProtocolTest.java @@ -0,0 +1,198 @@ +/* + * 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 com.alibaba.dubbo.rpc.protocol.http; + +import com.alibaba.dubbo.common.URL; +import com.alibaba.dubbo.common.beanutil.JavaBeanDescriptor; +import com.alibaba.dubbo.common.beanutil.JavaBeanSerializeUtil; +import com.alibaba.dubbo.common.extension.ExtensionLoader; +import com.alibaba.dubbo.common.serialize.ObjectInput; +import com.alibaba.dubbo.common.serialize.ObjectOutput; +import com.alibaba.dubbo.common.serialize.Serialization; +import com.alibaba.dubbo.common.serialize.nativejava.NativeJavaSerialization; +import com.alibaba.dubbo.rpc.*; +import com.alibaba.dubbo.rpc.service.GenericService; +import junit.framework.Assert; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.Assert.fail; + +/** + * HttpProtocolTest + */ +public class HttpProtocolTest { + + @Test + public void testHttpProtocol() { + HttpServiceImpl server = new HttpServiceImpl(); + Assert.assertFalse(server.isCalled()); + ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + URL url = URL.valueOf("http://127.0.0.1:5342/" + HttpService.class.getName() + "?version=1.0.0"); + Exporter exporter = protocol.export(proxyFactory.getInvoker(server, HttpService.class, url)); + Invoker invoker = protocol.refer(HttpService.class, url); + HttpService client = proxyFactory.getProxy(invoker); + String result = client.sayHello("haha"); + Assert.assertTrue(server.isCalled()); + Assert.assertEquals("Hello, haha", result); + invoker.destroy(); + exporter.unexport(); + } + + @Test + public void testGenericInvoke() { + HttpServiceImpl server = new HttpServiceImpl(); + Assert.assertFalse(server.isCalled()); + ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + URL url = URL.valueOf("http://127.0.0.1:5342/" + HttpService.class.getName() + "?version=1.0.0&generic=true"); + Exporter exporter = protocol.export(proxyFactory.getInvoker(server, HttpService.class, url)); + Invoker invoker = protocol.refer(GenericService.class, url); + GenericService client = proxyFactory.getProxy(invoker); + String result = (String) client.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"haha"}); + Assert.assertTrue(server.isCalled()); + Assert.assertEquals("Hello, haha", result); + invoker.destroy(); + exporter.unexport(); + } + + @Test + public void testGenericInvokeWithNativejava() throws IOException, ClassNotFoundException { + HttpServiceImpl server = new HttpServiceImpl(); + Assert.assertFalse(server.isCalled()); + ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + URL url = URL.valueOf("http://127.0.0.1:5342/" + HttpService.class.getName() + "?version=1.0.0&generic=nativejava"); + Exporter exporter = protocol.export(proxyFactory.getInvoker(server, HttpService.class, url)); + Invoker invoker = protocol.refer(GenericService.class, url); + GenericService client = proxyFactory.getProxy(invoker); + + Serialization serialization = new NativeJavaSerialization(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + ObjectOutput objectOutput = serialization.serialize(url, byteArrayOutputStream); + objectOutput.writeObject("haha"); + objectOutput.flushBuffer(); + + Object result = client.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{byteArrayOutputStream.toByteArray()}); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream((byte[]) result); + ObjectInput objectInput = serialization.deserialize(url, byteArrayInputStream); + Assert.assertTrue(server.isCalled()); + Assert.assertEquals("Hello, haha", objectInput.readObject()); + invoker.destroy(); + exporter.unexport(); + } + + @Test + public void testGenericInvokeWithBean() { + HttpServiceImpl server = new HttpServiceImpl(); + Assert.assertFalse(server.isCalled()); + ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + URL url = URL.valueOf("http://127.0.0.1:5342/" + HttpService.class.getName() + "?version=1.0.0&generic=bean"); + Exporter exporter = protocol.export(proxyFactory.getInvoker(server, HttpService.class, url)); + Invoker invoker = protocol.refer(GenericService.class, url); + GenericService client = proxyFactory.getProxy(invoker); + + JavaBeanDescriptor javaBeanDescriptor = JavaBeanSerializeUtil.serialize("haha"); + + Object result = client.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{javaBeanDescriptor}); + Assert.assertTrue(server.isCalled()); + Assert.assertEquals("Hello, haha", JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) result)); + invoker.destroy(); + exporter.unexport(); + } + + @Test + public void testOverload() { + HttpServiceImpl server = new HttpServiceImpl(); + Assert.assertFalse(server.isCalled()); + ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + URL url = URL.valueOf("http://127.0.0.1:5342/" + HttpService.class.getName() + "?version=1.0.0&hessian.overload.method=true&hessian2.request=false"); + Exporter exporter = protocol.export(proxyFactory.getInvoker(server, HttpService.class, url)); + Invoker invoker = protocol.refer(HttpService.class, url); + HttpService client = proxyFactory.getProxy(invoker); + String result = client.sayHello("haha"); + Assert.assertEquals("Hello, haha", result); + result = client.sayHello("haha", 1); + Assert.assertEquals("Hello, haha. ", result); + invoker.destroy(); + exporter.unexport(); + } + + @Test + public void testSimpleClient() { + HttpServiceImpl server = new HttpServiceImpl(); + Assert.assertFalse(server.isCalled()); + ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + URL url = URL.valueOf("http://127.0.0.1:5342/" + HttpService.class.getName() + "?version=1.0.0&client=simple"); + Exporter exporter = protocol.export(proxyFactory.getInvoker(server, HttpService.class, url)); + Invoker invoker = protocol.refer(HttpService.class, url); + HttpService client = proxyFactory.getProxy(invoker); + String result = client.sayHello("haha"); + Assert.assertTrue(server.isCalled()); + Assert.assertEquals("Hello, haha", result); + invoker.destroy(); + exporter.unexport(); + } + + @Test + public void testTimeOut() { + HttpServiceImpl server = new HttpServiceImpl(); + ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + URL url = URL.valueOf("http://127.0.0.1:5342/" + HttpService.class.getName() + "?version=1.0.0&timeout=10"); + Exporter exporter = protocol.export(proxyFactory.getInvoker(server, HttpService.class, url)); + Invoker invoker = protocol.refer(HttpService.class, url); + HttpService client = proxyFactory.getProxy(invoker); + try { + client.timeOut(6000); + fail(); + } catch (RpcException expected) { + Assert.assertEquals(true, expected.isTimeout()); + } finally { + invoker.destroy(); + exporter.unexport(); + } + + } + + @Test + public void testCustomException() { + HttpServiceImpl server = new HttpServiceImpl(); + ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + URL url = URL.valueOf("http://127.0.0.1:5342/" + HttpService.class.getName() + "?version=1.0.0"); + Exporter exporter = protocol.export(proxyFactory.getInvoker(server, HttpService.class, url)); + Invoker invoker = protocol.refer(HttpService.class, url); + HttpService client = proxyFactory.getProxy(invoker); + try { + client.customException(); + fail(); + } catch (HttpServiceImpl.MyException expected) { + } + invoker.destroy(); + exporter.unexport(); + } + +} diff --git a/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpService.java b/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpService.java new file mode 100644 index 00000000000..0d0d22c1e76 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpService.java @@ -0,0 +1,33 @@ +/* + * 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 com.alibaba.dubbo.rpc.protocol.http; + + +/** + * HttpService + */ +public interface HttpService { + + String sayHello(String name); + + String sayHello(String name, int times); + + void timeOut(int millis); + + String customException(); + +} diff --git a/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpServiceImpl.java b/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpServiceImpl.java new file mode 100644 index 00000000000..1c78b3ba692 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-http/src/test/java/com/alibaba/dubbo/rpc/protocol/http/HttpServiceImpl.java @@ -0,0 +1,64 @@ +/* + * 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 com.alibaba.dubbo.rpc.protocol.http; + +/** + * HttpServiceImpl + */ +public class HttpServiceImpl implements HttpService { + + private boolean called; + + public String sayHello(String name) { + called = true; + return "Hello, " + name; + } + + public String sayHello(String name, int times) { + called = true; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < times; i++) { + sb.append("Hello, " + name + ". "); + } + return sb.toString(); + } + + public boolean isCalled() { + return called; + } + + public void timeOut(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public String customException() { + throw new MyException("custom exception"); + } + + static class MyException extends RuntimeException { + + private static final long serialVersionUID = -3051041116483629056L; + + public MyException(String message) { + super(message); + } + } +}