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);
+ }
+ }
+}