INT-3820: Fix MethodInvokerHelper for the Proxy

JIRA: https://jira.spring.io/browse/INT-3820

Some Proxy may be based on the specif non-user classes and advised with
the provided end-user interfaces.
The best sample is `@Repository` from Spring Data projects.
In this case the `MessagingMethodInvokerHelper` just missed the user interfaces
to consider for the candidate methods or thrown an `Exception` like `NoSuchMethodError`.

* Add `((Advised) targetObject).getProxiedInterfaces()` for the scanning algorithm
* Move the `UniqueMethodFilter` to the internal `Set<Method>` to allow iteration
and filtering for methods from the `targetClass` as well as from all those user interfaces
* Add Spring JPA repository test-case
* Polishing form some time-weak tests

Revert MessagingMethodInvokerHelper

INT-3820: Simple Proxy Interface Method Matching

No-risk solution for INT-3820.

Instead of considering all interfaces on the proxy, only look at proxy interfaces
for a single matching method name if we find no candidate or fallback methods.

Consider the complete solution for 4.3.

Add `Message<?>` method distinguishing
This commit is contained in:
Artem Bilan
2015-09-05 00:18:42 -04:00
committed by Gary Russell
parent ca62f18a7b
commit d8fc94602c
15 changed files with 139 additions and 37 deletions

View File

@@ -133,6 +133,7 @@ subprojects { subproject ->
smackVersion = '4.0.6'
springAmqpVersion = project.hasProperty('springAmqpVersion') ? project.springAmqpVersion : '1.5.0.RELEASE'
// springCloudClusterVersion = '1.0.0.BUILD-SNAPSHOT'
springDataJpaVersion = '1.8.2.RELEASE'
springDataMongoVersion = '1.7.2.RELEASE'
springDataRedisVersion = '1.5.2.RELEASE'
springGemfireVersion = '1.6.0.RELEASE'
@@ -452,6 +453,8 @@ project('spring-integration-jpa') {
compile ("org.eclipse.persistence:javax.persistence:$jpaApiVersion", optional)
testCompile "org.springframework.data:spring-data-jpa:$springDataJpaVersion"
testCompile "com.h2database:h2:$h2Version"
testCompile "org.hsqldb:hsqldb:$hsqldbVersion"
testCompile "org.apache.derby:derby:$derbyVersion"

View File

@@ -440,6 +440,12 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
}
}, methodFilter);
if (candidateMethods.isEmpty() && candidateMessageMethods.isEmpty() && fallbackMethods.isEmpty()
&& fallbackMessageMethods.isEmpty()) {
findSingleSpecifMethodOnInterfacesIfProxy(targetObject, methodName, candidateMessageMethods,
candidateMethods);
}
if (!candidateMethods.isEmpty() || !candidateMessageMethods.isEmpty()) {
handlerMethods.put(CANDIDATE_METHODS, candidateMethods);
handlerMethods.put(CANDIDATE_MESSAGE_METHODS, candidateMessageMethods);
@@ -499,6 +505,62 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
return handlerMethods;
}
private void findSingleSpecifMethodOnInterfacesIfProxy(final Object targetObject, final String methodName,
Map<Class<?>, HandlerMethod> candidateMessageMethods,
Map<Class<?>, HandlerMethod> candidateMethods) {
if (AopUtils.isAopProxy(targetObject)) {
final AtomicReference<Method> targetMethod = new AtomicReference<Method>();
Class<?>[] interfaces = ((Advised) targetObject).getProxiedInterfaces();
for (Class<?> clazz : interfaces) {
ReflectionUtils.doWithMethods(clazz, new MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
if (targetMethod.get() != null) {
throw new IllegalStateException("Ambiguous method " + methodName + " on " + targetObject);
}
else {
targetMethod.set(method);
}
}
}, new MethodFilter() {
@Override
public boolean matches(Method method) {
return method.getName().equals(methodName);
}
});
}
Method method = targetMethod.get();
if (method != null) {
HandlerMethod handlerMethod = new HandlerMethod(method, this.canProcessMessageList);
Class<?> targetParameterType = handlerMethod.getTargetParameterType();
if (handlerMethod.isMessageMethod()) {
if (candidateMessageMethods.containsKey(targetParameterType)) {
throw new IllegalArgumentException("Found more than one method match for type " +
"[Message<" + targetParameterType + ">]");
}
candidateMessageMethods.put(targetParameterType, handlerMethod);
}
else {
if (candidateMethods.containsKey(targetParameterType)) {
String exceptionMessage = "Found more than one method match for ";
if (Void.class.equals(targetParameterType)) {
exceptionMessage += "empty parameter for 'payload'";
}
else {
exceptionMessage += "type [" + targetParameterType + "]";
}
throw new IllegalArgumentException(exceptionMessage);
}
candidateMethods.put(targetParameterType, handlerMethod);
}
}
}
}
private Class<?> getTargetClass(Object targetObject) {
Class<?> targetClass = targetObject.getClass();
if (AopUtils.isAopProxy(targetObject)) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -77,7 +77,7 @@ public class DispatchingChannelErrorHandlingTests {
});
Message<?> message = MessageBuilder.withPayload("test").build();
channel.send(message);
this.waitForLatch(1000);
this.waitForLatch(10000);
Message<?> errorMessage = resultHandler.lastMessage;
assertEquals(MessagingException.class, errorMessage.getPayload().getClass());
MessagingException exceptionPayload = (MessagingException) errorMessage.getPayload();
@@ -108,7 +108,7 @@ public class DispatchingChannelErrorHandlingTests {
});
Message<?> message = MessageBuilder.withPayload("test").build();
channel.send(message);
this.waitForLatch(1000);
this.waitForLatch(10000);
Message<?> errorMessage = resultHandler.lastMessage;
assertEquals(MessagingException.class, errorMessage.getPayload().getClass());
MessagingException exceptionPayload = (MessagingException) errorMessage.getPayload();

View File

@@ -386,7 +386,7 @@ public class GatewayParserTests {
public <T> Future<T> submit(Callable<T> task) {
try {
Future<?> result = super.submit(task);
Message<?> message = (Message<?>) result.get(1, TimeUnit.SECONDS);
Message<?> message = (Message<?>) result.get(10, TimeUnit.SECONDS);
Message<?> modifiedMessage;
if (message == null) {
modifiedMessage = MessageBuilder.withPayload("foo")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,7 +47,7 @@ public class PollerWithErrorChannelTests {
* the ErrorMessage will still be forwarded to the 'errorChannel' since exception occurs on
* receive() and not on send()
*/
public void testWithErrorChannelAsHeader() throws Exception{
public void testWithErrorChannelAsHeader() throws Exception {
ApplicationContext ac = new ClassPathXmlApplicationContext("PollerWithErrorChannel-context.xml", this.getClass());
SourcePollingChannelAdapter adapter = ac.getBean("withErrorHeader", SourcePollingChannelAdapter.class);
@@ -61,49 +61,52 @@ public class PollerWithErrorChannelTests {
}
@Test
public void testWithErrorChannel() throws Exception{
public void testWithErrorChannel() throws Exception {
ApplicationContext ac = new ClassPathXmlApplicationContext("PollerWithErrorChannel-context.xml", this.getClass());
SourcePollingChannelAdapter adapter = ac.getBean("withErrorChannel", SourcePollingChannelAdapter.class);
adapter.start();
PollableChannel errorChannel = ac.getBean("eChannel", PollableChannel.class);
assertNotNull(errorChannel.receive(1000));
assertNotNull(errorChannel.receive(10000));
adapter.stop();
}
@Test
public void testWithErrorChannelAndHeader() throws Exception{
public void testWithErrorChannelAndHeader() throws Exception {
ApplicationContext ac = new ClassPathXmlApplicationContext("PollerWithErrorChannel-context.xml", this.getClass());
SourcePollingChannelAdapter adapter = ac.getBean("withErrorChannelAndHeader", SourcePollingChannelAdapter.class);
adapter.start();
PollableChannel errorChannel = ac.getBean("eChannel", PollableChannel.class);
assertNotNull(errorChannel.receive(1000));
assertNotNull(errorChannel.receive(10000));
adapter.stop();
}
@Test
// config the same as above but the error wil come from the send
public void testWithErrorChannelAndHeaderWithSendFailure() throws Exception{
public void testWithErrorChannelAndHeaderWithSendFailure() throws Exception {
ApplicationContext ac = new ClassPathXmlApplicationContext("PollerWithErrorChannel-context.xml", this.getClass());
SourcePollingChannelAdapter adapter = ac.getBean("withErrorChannelAndHeaderErrorOnSend", SourcePollingChannelAdapter.class);
adapter.start();
PollableChannel errorChannel = ac.getBean("errChannel", PollableChannel.class);
assertNotNull(errorChannel.receive(1000));
assertNotNull(errorChannel.receive(10000));
adapter.stop();
}
@Test
// INT-1952
public void testWithErrorChannelAndPollingConsumer() throws Exception{
public void testWithErrorChannelAndPollingConsumer() throws Exception {
ApplicationContext ac = new ClassPathXmlApplicationContext("PollerWithErrorChannel-context.xml", this.getClass());
MessageChannel serviceWithPollerChannel = ac.getBean("serviceWithPollerChannel", MessageChannel.class);
QueueChannel errChannel = ac.getBean("serviceErrorChannel", QueueChannel.class);
QueueChannel errorChannel = ac.getBean("serviceErrorChannel", QueueChannel.class);
serviceWithPollerChannel.send(new GenericMessage<String>(""));
assertNotNull(errChannel.receive(1000));
assertNotNull(errorChannel.receive(10000));
}
public static class SampleService{
public static class SampleService {
public String withSuccess(){
return "hello";
}
}
}

View File

@@ -243,6 +243,7 @@ public class DelayHandlerTests {
final CountDownLatch latch = new CountDownLatch(1);
new Thread(new Runnable() {
@Override
public void run() {
try {
@@ -253,9 +254,10 @@ public class DelayHandlerTests {
// won't countDown
}
}
}).start();
latch.await(50, TimeUnit.MILLISECONDS);
assertEquals(0, latch.getCount());
assertTrue(latch.await(1, TimeUnit.SECONDS));
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -63,7 +63,7 @@ public abstract class AbstractPersistentAcceptOnceFileListFilter<F> extends Abst
/**
* Determine whether the metadataStore should be flushed on each update (if {@link Flushable}).
* @param flushOnUpdate true to flush.
* @since 1.4.5
* @since 4.1.5
*/
public void setFlushOnUpdate(boolean flushOnUpdate) {
this.flushOnUpdate = flushOnUpdate;

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.jms;
import static org.junit.Assert.assertFalse;
@@ -401,6 +402,8 @@ public class OutboundGatewayFunctionTests {
gateway.setCorrelationKey("JMSCorrelationID");
gateway.setUseReplyContainer(true);
gateway.setIdleReplyContainerTimeout(1, TimeUnit.SECONDS);
gateway.setRequiresReply(true);
gateway.setReceiveTimeout(10000);
gateway.afterPropertiesSet();
gateway.start();
Executors.newSingleThreadExecutor().execute(new Runnable() {

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.jmx.config;
import static org.junit.Assert.assertEquals;
@@ -24,7 +25,6 @@ import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.QueryExp;
@@ -103,10 +103,7 @@ public class MBeanTreePollingChannelAdapterParserTests {
@Autowired
private MBeanObjectConverter converter;
@Autowired
private MBeanServer mbeanServer;
private final long testTimeout = 2000L;
private final long testTimeout = 20000L;
@Test
public void pollDefaultAdapter() throws Exception {

View File

@@ -1,19 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jpa="http://www.springframework.org/schema/integration/jpa"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jpa="http://www.springframework.org/schema/integration/jpa"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:sd-jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/jpa http://www.springframework.org/schema/integration/jpa/spring-integration-jpa.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<sd-jpa:repositories base-package="org.springframework.integration.jpa.outbound"/>
<int:service-activator input-channel="jpaInputChannel" ref="studentRepository" method="findByGender"/>
<import resource="BaseJpaPollingChannelAdapterTests-context.xml"/>
@@ -33,6 +35,7 @@
<int:method name="persistStudent2" request-channel="updatingGatewayInsideChain" />
<int:method name="getAllStudentsFromGivenRecord" request-channel="getStudentsFromGivenRecordChannel"/>
<int:method name="getStudents" request-channel="getStudentsWithMaxNumberOfRecordsChannel"/>
<int:method name="getStudentsUsingJpaRepository" request-channel="jpaInputChannel"/>
</int:gateway>
<int:channel id="studentReplyChannel"/>

View File

@@ -169,4 +169,10 @@ public class JpaOutboundGatewayTests {
}
@Test
public void testJpaRepositoryAsService() {
List<StudentDomain> students = this.studentService.getStudentsUsingJpaRepository("F");
Assert.assertEquals(2, students.size());
}
}

View File

@@ -0,0 +1,18 @@
package org.springframework.integration.jpa.outbound;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.integration.jpa.test.entity.StudentDomain;
import org.springframework.stereotype.Repository;
/**
* @author Artem Bilan
* @since 4.2
*/
@Repository
public interface StudentRepository extends JpaRepository<StudentDomain, Long> {
List<StudentDomain> findByGender(String gender);
}

View File

@@ -52,4 +52,7 @@ public interface StudentService {
StudentDomain persistStudent2(StudentDomain studentToPersist);
List<StudentDomain> getStudents(int maxNumberOfRecords);
List<StudentDomain> getStudentsUsingJpaRepository(String gender);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -201,6 +201,7 @@ public class CharacterStreamWritingMessageHandlerTests {
public String toString() {
return this.text;
}
}
@@ -225,7 +226,7 @@ public class CharacterStreamWritingMessageHandlerTests {
public void await() {
try {
this.latch.await(1000, TimeUnit.MILLISECONDS);
this.latch.await(10000, TimeUnit.MILLISECONDS);
if (latch.getCount() != 0) {
throw new RuntimeException("test timeout");
}
@@ -234,6 +235,7 @@ public class CharacterStreamWritingMessageHandlerTests {
throw new RuntimeException("test latch.await() interrupted");
}
}
}
}