diff --git a/README.md b/README.md index ea800db1..d49dbf2b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ JSON-RPC). * HTTP Server (`HttpServletRequest` \ `HttpServletResponse`) * Portlet Server (`ResourceRequest` \ `ResourceResponse`) * Socket Server (`StreamServer`) - * Integration with the Spring Framework (`RemoteExporter`) + * Integration with the Spring Framework * Streaming client * HTTP client * Dynamic client proxies @@ -66,7 +66,7 @@ that take `InputStream`s and `OutputStream`s. Also in the library is a `JsonRpc which extends the `JsonRpcClient` to add HTTP support. ## Spring Framework -jsonrpc4j provides a `RemoteExporter` to expose java services as JSON-RPC over HTTP without +jsonrpc4j provides support for exposing java services as JSON-RPC over HTTP without requiring any additional work on the part of the programmer. The following example explains how to use the `JsonServiceExporter` within the Spring Framework. @@ -113,7 +113,9 @@ public class UserServiceImpl } ``` -Configure your service in spring as you would any other RemoteExporter: +Configure your service in Spring as you would do it for any other Beans, +and then add a reference of your service Bean into `JsonServiceExporter` +by specifying the `service` and `serviceInterface` properties: ```xml diff --git a/src/main/java/com/googlecode/jsonrpc4j/spring/AbstractCompositeJsonServiceExporter.java b/src/main/java/com/googlecode/jsonrpc4j/spring/AbstractCompositeJsonServiceExporter.java index 8d2fb6cd..50cfd59c 100644 --- a/src/main/java/com/googlecode/jsonrpc4j/spring/AbstractCompositeJsonServiceExporter.java +++ b/src/main/java/com/googlecode/jsonrpc4j/spring/AbstractCompositeJsonServiceExporter.java @@ -93,7 +93,7 @@ public final void afterPropertiesSet() * * @throws Exception on error */ - void exportService() + protected void exportService() throws Exception { // no-op } diff --git a/src/main/java/com/googlecode/jsonrpc4j/spring/AbstractJsonServiceExporter.java b/src/main/java/com/googlecode/jsonrpc4j/spring/AbstractJsonServiceExporter.java index ba145534..e9977117 100644 --- a/src/main/java/com/googlecode/jsonrpc4j/spring/AbstractJsonServiceExporter.java +++ b/src/main/java/com/googlecode/jsonrpc4j/spring/AbstractJsonServiceExporter.java @@ -2,23 +2,25 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.googlecode.jsonrpc4j.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.remoting.support.RemoteExporter; +import org.springframework.util.ClassUtils; import java.util.List; import java.util.concurrent.ExecutorService; /** - * {@link RemoteExporter} that exports services using Json - * according to the JSON-RPC proposal specified at: - * - * http://groups.google.com/group/json-rpc. + * Exports user defined services using JSON-RPC protocol */ @SuppressWarnings("unused") -abstract class AbstractJsonServiceExporter extends RemoteExporter implements InitializingBean, ApplicationContextAware { +abstract class AbstractJsonServiceExporter implements InitializingBean, ApplicationContextAware { + private static final Logger logger = LoggerFactory.getLogger(AbstractJsonServiceExporter.class); private ObjectMapper objectMapper; private JsonRpcServer jsonRpcServer; @@ -36,6 +38,8 @@ abstract class AbstractJsonServiceExporter extends RemoteExporter implements Ini private List interceptorList; private ExecutorService batchExecutorService = null; private long parallelBatchProcessingTimeout; + private Object service; + private Class serviceInterface; /** * {@inheritDoc} @@ -49,7 +53,7 @@ public void afterPropertiesSet() throws Exception { try { objectMapper = BeanFactoryUtils.beanOfTypeIncludingAncestors(applicationContext, ObjectMapper.class); } catch (Exception e) { - logger.debug(e); + logger.debug("Failed to obtain objectMapper from application context", e); } } if (objectMapper == null) { @@ -93,7 +97,7 @@ public void afterPropertiesSet() throws Exception { * * @throws Exception on error */ - void exportService() + protected void exportService() throws Exception { // no-op } @@ -214,4 +218,96 @@ public void setBatchExecutorService(ExecutorService batchExecutorService) { public void setParallelBatchProcessingTimeout(long parallelBatchProcessingTimeout) { this.parallelBatchProcessingTimeout = parallelBatchProcessingTimeout; } + + /** + * Set the service to export. + * Typically populated via a bean reference. + */ + public void setService(Object service) { + this.service = service; + } + + /** + * Return the service to export. + */ + public Object getService() { + return this.service; + } + + /** + * Set the interface of the service to export. + * The interface must be suitable for the particular service and remoting strategy. + */ + public void setServiceInterface(Class serviceInterface) { + if (serviceInterface == null) { + throw new IllegalArgumentException("'serviceInterface' must not be null"); + } + if (!serviceInterface.isInterface()) { + throw new IllegalArgumentException("'serviceInterface' must be an interface"); + } + this.serviceInterface = serviceInterface; + } + + /** + * Return the interface of the service to export. + */ + public Class getServiceInterface() { + return this.serviceInterface; + } + + + /** + * Check whether a service reference has been set, + * and whether it matches the specified service. + * @see #setServiceInterface + * @see #setService + */ + protected void checkServiceInterface() throws IllegalArgumentException { + Class serviceInterface = getServiceInterface(); + if (serviceInterface == null) { + throw new IllegalArgumentException("Property 'serviceInterface' is required"); + } + + Object service = getService(); + if (service instanceof String) { + throw new IllegalArgumentException( + "Service [" + service + "] is a String rather than an actual service reference:" + + " Have you accidentally specified the service bean name as value " + + " instead of as reference?" + ); + } + if (!serviceInterface.isInstance(service)) { + throw new IllegalArgumentException( + "Service interface [" + serviceInterface.getName() + + "] needs to be implemented by service [" + service + "] of class [" + + service.getClass().getName() + "]" + ); + } + } + + + /** + * Get a proxy for the given service object, implementing the specified + * service interface. + *

Used to export a proxy that does not expose any internals but just + * a specific interface intended for remote access. + * + * @return the proxy + * @see #setServiceInterface + * @see #setService + */ + protected Object getProxyForService() { + Object targetService = getService(); + if (targetService == null) { + throw new IllegalArgumentException("Property 'service' is required"); + } + checkServiceInterface(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addInterface(getServiceInterface()); + proxyFactory.setTarget(targetService); + proxyFactory.setOpaque(true); + + return proxyFactory.getProxy(ClassUtils.getDefaultClassLoader()); + } } diff --git a/src/main/java/com/googlecode/jsonrpc4j/spring/AutoJsonRpcServiceExporter.java b/src/main/java/com/googlecode/jsonrpc4j/spring/AutoJsonRpcServiceExporter.java index 1e27c798..83e9c7a9 100644 --- a/src/main/java/com/googlecode/jsonrpc4j/spring/AutoJsonRpcServiceExporter.java +++ b/src/main/java/com/googlecode/jsonrpc4j/spring/AutoJsonRpcServiceExporter.java @@ -44,7 +44,6 @@ public class AutoJsonRpcServiceExporter implements BeanFactoryPostProcessor { private ObjectMapper objectMapper; private ErrorResolver errorResolver = null; - private Boolean registerTraceInterceptor; private boolean backwardsCompatible = true; private boolean rethrowExceptions = false; private boolean allowExtraParams = false; @@ -141,10 +140,6 @@ private void registerServiceProxy(DefaultListableBeanFactory defaultListableBean builder.addPropertyValue("invocationListener", invocationListener); } - if (registerTraceInterceptor != null) { - builder.addPropertyValue("registerTraceInterceptor", registerTraceInterceptor); - } - if (httpStatusCodeProvider != null) { builder.addPropertyValue("httpStatusCodeProvider", httpStatusCodeProvider); } @@ -225,12 +220,16 @@ public void setAllowLessParams(boolean allowLessParams) { } /** - * See {@link org.springframework.remoting.support.RemoteExporter#setRegisterTraceInterceptor(boolean)} + * See {@code org.springframework.remoting.support.RemoteExporter#setRegisterTraceInterceptor(boolean)} + *

+ * Note: this method is deprecated and marked for removal. + * {@code RemoteExporter} and {@code TraceInterceptor-s} are no longer supported. * * @param registerTraceInterceptor the registerTraceInterceptor value to set */ + @Deprecated public void setRegisterTraceInterceptor(boolean registerTraceInterceptor) { - this.registerTraceInterceptor = registerTraceInterceptor; + // NOOP } /** diff --git a/src/main/java/com/googlecode/jsonrpc4j/spring/AutoJsonRpcServiceImplExporter.java b/src/main/java/com/googlecode/jsonrpc4j/spring/AutoJsonRpcServiceImplExporter.java index db9cad1a..ad355395 100644 --- a/src/main/java/com/googlecode/jsonrpc4j/spring/AutoJsonRpcServiceImplExporter.java +++ b/src/main/java/com/googlecode/jsonrpc4j/spring/AutoJsonRpcServiceImplExporter.java @@ -50,7 +50,6 @@ public class AutoJsonRpcServiceImplExporter implements BeanFactoryPostProcessor private ObjectMapper objectMapper; private ErrorResolver errorResolver = null; - private Boolean registerTraceInterceptor; private boolean backwardsCompatible = true; private boolean rethrowExceptions = false; private boolean allowExtraParams = false; @@ -181,11 +180,7 @@ private void registerServiceProxy(DefaultListableBeanFactory defaultListableBean if (invocationListener != null) { builder.addPropertyValue("invocationListener", invocationListener); } - - if (registerTraceInterceptor != null) { - builder.addPropertyValue("registerTraceInterceptor", registerTraceInterceptor); - } - + if (httpStatusCodeProvider != null) { builder.addPropertyValue("httpStatusCodeProvider", httpStatusCodeProvider); } @@ -280,14 +275,18 @@ public void setAllowExtraParams(boolean allowExtraParams) { public void setAllowLessParams(boolean allowLessParams) { this.allowLessParams = allowLessParams; } - + /** - * See {@link org.springframework.remoting.support.RemoteExporter#setRegisterTraceInterceptor(boolean)} + * See {@code org.springframework.remoting.support.RemoteExporter#setRegisterTraceInterceptor(boolean)} + *

+ * Note: this method is deprecated and marked for removal. + * {@code RemoteExporter} and {@code TraceInterceptor-s} are no longer supported. * * @param registerTraceInterceptor the registerTraceInterceptor value to set */ + @Deprecated public void setRegisterTraceInterceptor(boolean registerTraceInterceptor) { - this.registerTraceInterceptor = registerTraceInterceptor; + // NOOP } /** diff --git a/src/main/java/com/googlecode/jsonrpc4j/spring/JsonServiceExporter.java b/src/main/java/com/googlecode/jsonrpc4j/spring/JsonServiceExporter.java index 7bc2ab53..e43cd1ac 100644 --- a/src/main/java/com/googlecode/jsonrpc4j/spring/JsonServiceExporter.java +++ b/src/main/java/com/googlecode/jsonrpc4j/spring/JsonServiceExporter.java @@ -9,10 +9,7 @@ import java.io.IOException; /** - * {@link HttpRequestHandler} that exports services using Json - * according to the JSON-RPC proposal specified at: - * - * http://groups.google.com/group/json-rpc. + * {@link HttpRequestHandler} that exports user services using JSON-RPC over HTTP protocol */ public class JsonServiceExporter extends AbstractJsonServiceExporter implements HttpRequestHandler { diff --git a/src/main/java/com/googlecode/jsonrpc4j/spring/JsonStreamServiceExporter.java b/src/main/java/com/googlecode/jsonrpc4j/spring/JsonStreamServiceExporter.java index ae62351e..7469c469 100644 --- a/src/main/java/com/googlecode/jsonrpc4j/spring/JsonStreamServiceExporter.java +++ b/src/main/java/com/googlecode/jsonrpc4j/spring/JsonStreamServiceExporter.java @@ -11,11 +11,7 @@ /** - * {@link org.springframework.remoting.support.RemoteExporter RemoteExporter} - * that exports services using Json according to the JSON-RPC proposal specified - * at: - * - * http://groups.google.com/group/json-rpc. + * Exports user defined services as streaming server, which provides JSON-RPC over sockets. */ @SuppressWarnings("unused") public class JsonStreamServiceExporter extends AbstractJsonServiceExporter implements DisposableBean { diff --git a/src/test/java/com/googlecode/jsonrpc4j/spring/JsonServiceExporterIntegrationTest.java b/src/test/java/com/googlecode/jsonrpc4j/spring/JsonServiceExporterIntegrationTest.java new file mode 100644 index 00000000..dfd36248 --- /dev/null +++ b/src/test/java/com/googlecode/jsonrpc4j/spring/JsonServiceExporterIntegrationTest.java @@ -0,0 +1,53 @@ +package com.googlecode.jsonrpc4j.spring; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.googlecode.jsonrpc4j.spring.service.Service; +import com.googlecode.jsonrpc4j.spring.service.ServiceImpl; + +import static org.junit.Assert.*; + +/** + * This test ensures that {@link com.googlecode.jsonrpc4j.spring.JsonServiceExporter} bean is + * constructed according to Spring Framework configuration. + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:serverApplicationContextC.xml") +public class JsonServiceExporterIntegrationTest { + + private static final String BEAN_NAME_AND_URL_PATH = "/UserService.json"; + + @Autowired + private ApplicationContext applicationContext; + + @Test + public void testExportedService() { + assertNotNull(applicationContext); + + // check that the bean was only exported on the configured path. + { + Object bean = applicationContext.getBean(BEAN_NAME_AND_URL_PATH); + assertEquals(JsonServiceExporter.class, bean.getClass()); + + String[] names = applicationContext.getBeanNamesForType(JsonServiceExporter.class); + assertNotNull(names); + assertEquals(1, names.length); + assertEquals(BEAN_NAME_AND_URL_PATH, names[0]); + } + + // check that service classes were also successfully configured in the context. + + { + Service service = applicationContext.getBean(Service.class); + assertTrue(service instanceof ServiceImpl); + + ServiceImpl serviceImpl = applicationContext.getBean(ServiceImpl.class); + assertNotNull(serviceImpl); + } + } +} diff --git a/src/test/resources/serverApplicationContextC.xml b/src/test/resources/serverApplicationContextC.xml new file mode 100644 index 00000000..ed2c0156 --- /dev/null +++ b/src/test/resources/serverApplicationContextC.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + +