This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karaf.git


The following commit(s) were added to refs/heads/main by this push:
     new 7195db13d fix(#565): Fix bean method resolution for OSGi Blueprint 
proxy classes (#689)
7195db13d is described below

commit 7195db13d9aca38b29406cd94ce42d3b96404b6c
Author: JB Onofré <[email protected]>
AuthorDate: Thu Mar 12 19:53:50 2026 +0100

    fix(#565): Fix bean method resolution for OSGi Blueprint proxy classes 
(#689)
    
    * fix(#565): Fix bean method resolution for OSGi Blueprint proxy classes
    
    The isSynthetic() guard in BeanInfo.findMostSpecificOverride() prevents
    detection of method overrides from synthetic classes. This causes both the
    proxy's and the interface's method to be registered as separate operations,
    leading to AmbiguousMethodCallException when invoking methods on OSGi
    Blueprint service reference proxies.
    
    Add a new branch that detects overrides from non-hidden synthetic classes
    implementing interfaces (such as Blueprint proxies) while still excluding
    hidden synthetic classes (lambdas, method references) to preserve annotation
    discovery.
    
    * fix(#565): Restrict OSGi exports and make io.quarkus.arc import optional 
for camel-bean
    
    The provided dependencies (camel-api, camel-support, camel-util) caused the
    maven-bundle-plugin to export all org.apache.camel packages and generate a
    mandatory import for io.quarkus.arc, breaking Karaf feature resolution.
---
 components/camel-bean/pom.xml                      |   49 +-
 .../org/apache/camel/component/bean/BeanInfo.java  | 1398 ++++++++++++++++++++
 .../component/bean/BeanInfoSyntheticProxyTest.java |  165 +++
 3 files changed, 1611 insertions(+), 1 deletion(-)

diff --git a/components/camel-bean/pom.xml b/components/camel-bean/pom.xml
index a59ee372a..54c4f84bc 100644
--- a/components/camel-bean/pom.xml
+++ b/components/camel-bean/pom.xml
@@ -34,9 +34,12 @@
 
     <properties>
         <camel.osgi.export>
-            org.apache.camel*;version=${camel-version}
+            org.apache.camel.component.bean*;version=${camel-version},
+            org.apache.camel.component.beanclass*;version=${camel-version},
+            org.apache.camel.language.bean*;version=${camel-version}
         </camel.osgi.export>
         <camel.osgi.import>
+            io.quarkus.arc;resolution:=optional,
             *
         </camel.osgi.import>
     </properties>
@@ -53,6 +56,50 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <!-- Compile-only dependencies needed for the patched BeanInfo -->
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-api</artifactId>
+            <version>${camel-version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-support</artifactId>
+            <version>${camel-version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-util</artifactId>
+            <version>${camel-version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-core</artifactId>
+            <version>${camel-version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.bytebuddy</groupId>
+            <artifactId>byte-buddy</artifactId>
+            <version>${bytebuddy-version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>${junit-jupiter-version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>${slf4j-version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
new file mode 100644
index 000000000..282c429ac
--- /dev/null
+++ 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
@@ -0,0 +1,1398 @@
+/*
+ * 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.camel.component.bean;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.Body;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangeException;
+import org.apache.camel.ExchangeProperties;
+import org.apache.camel.ExchangeProperty;
+import org.apache.camel.Expression;
+import org.apache.camel.Handler;
+import org.apache.camel.Header;
+import org.apache.camel.Headers;
+import org.apache.camel.Message;
+import org.apache.camel.PropertyInject;
+import org.apache.camel.Variable;
+import org.apache.camel.Variables;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.ObjectHelper;
+import org.apache.camel.support.builder.ExpressionBuilder;
+import org.apache.camel.support.language.AnnotationExpressionFactory;
+import org.apache.camel.support.language.DefaultAnnotationExpressionFactory;
+import org.apache.camel.support.language.LanguageAnnotation;
+import org.apache.camel.util.CastUtils;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.StringQuoteHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static 
org.apache.camel.component.bean.ParameterMappingStrategyHelper.createParameterMappingStrategy;
+
+/**
+ * Represents the metadata about a bean type created via a combination of 
introspection and annotations together with
+ * some useful sensible defaults
+ */
+public class BeanInfo {
+    private static final Logger LOG = LoggerFactory.getLogger(BeanInfo.class);
+    private static final String CGLIB_CLASS_SEPARATOR = "$$";
+    private static final String CGLIB_METHOD_MARKER = "CGLIB$";
+    private static final String BYTE_BUDDY_METHOD_MARKER = "$accessor$";
+    private static final String CLIENT_PROXY_SUFFIX = "_ClientProxy";
+    private static final String SUBCLASS_SUFFIX = "_Subclass";
+    private static final String[] EXCLUDED_METHOD_NAMES = new String[] {
+            "equals", "finalize", "getClass", "hashCode", "notify", 
"notifyAll", "wait", // java.lang.Object
+            "getInvocationHandler", "getProxyClass", "isProxyClass", 
"newProxyInstance" // java.lang.Proxy
+    };
+    private final CamelContext camelContext;
+    private final BeanComponent component;
+    private final Class<?> type;
+    private final ParameterMappingStrategy strategy;
+    private final MethodInfo defaultMethod;
+    // shared state with details of operations introspected from the bean, 
created during the constructor
+    private Map<String, List<MethodInfo>> operations = new HashMap<>();
+    private List<MethodInfo> operationsWithBody = new ArrayList<>();
+    private List<MethodInfo> operationsWithNoBody = new ArrayList<>();
+    private List<MethodInfo> operationsWithCustomAnnotation = new 
ArrayList<>();
+    private List<MethodInfo> operationsWithHandlerAnnotation = new 
ArrayList<>();
+    private Map<Method, MethodInfo> methodMap = new HashMap<>();
+    private boolean publicConstructors;
+    private boolean publicNoArgConstructors;
+
+    public BeanInfo(CamelContext camelContext, Class<?> type) {
+        this(camelContext, type, createParameterMappingStrategy(camelContext),
+             camelContext.getComponent("bean", BeanComponent.class));
+    }
+
+    public BeanInfo(CamelContext camelContext, Method explicitMethod, 
ParameterMappingStrategy parameterMappingStrategy,
+                    BeanComponent beanComponent) {
+        this(camelContext, explicitMethod.getDeclaringClass(), null, 
explicitMethod, parameterMappingStrategy, beanComponent);
+    }
+
+    public BeanInfo(CamelContext camelContext, Class<?> type, 
ParameterMappingStrategy strategy, BeanComponent beanComponent) {
+        this(camelContext, type, null, null, strategy, beanComponent);
+    }
+
+    public BeanInfo(CamelContext camelContext, Class<?> type, Object instance, 
Method explicitMethod,
+                    ParameterMappingStrategy strategy,
+                    BeanComponent beanComponent) {
+
+        this.camelContext = camelContext;
+        this.type = type;
+        this.strategy = strategy;
+        this.component = beanComponent;
+
+        final BeanInfoCacheKey key = new BeanInfoCacheKey(type, instance, 
explicitMethod);
+        final BeanInfoCacheKey key2 = instance != null ? new 
BeanInfoCacheKey(type, null, explicitMethod) : null;
+
+        // lookup if we have a bean info cache
+        BeanInfo beanInfo = component.getBeanInfoFromCache(key);
+        if (key2 != null && beanInfo == null) {
+            beanInfo = component.getBeanInfoFromCache(key2);
+        }
+        if (beanInfo != null) {
+            // copy the values from the cache we need
+            defaultMethod = beanInfo.defaultMethod;
+            operations = beanInfo.operations;
+            operationsWithBody = beanInfo.operationsWithBody;
+            operationsWithNoBody = beanInfo.operationsWithNoBody;
+            operationsWithCustomAnnotation = 
beanInfo.operationsWithCustomAnnotation;
+            operationsWithHandlerAnnotation = 
beanInfo.operationsWithHandlerAnnotation;
+            methodMap = beanInfo.methodMap;
+            publicConstructors = beanInfo.publicConstructors;
+            publicNoArgConstructors = beanInfo.publicNoArgConstructors;
+            return;
+        }
+
+        if (explicitMethod != null) {
+            // must be a valid method
+            if (!isValidMethod(type, explicitMethod)) {
+                throw new IllegalArgumentException(
+                        "The method " + explicitMethod + " is not valid (for 
example the method must be public)");
+            }
+            introspect(getType(), explicitMethod);
+        } else {
+            introspect(getType());
+        }
+
+        // if there are only 1 method with 1 operation then select it as a 
default/fallback method
+        MethodInfo method = null;
+        if (operations.size() == 1) {
+            List<MethodInfo> methods = operations.values().iterator().next();
+            if (methods.size() == 1) {
+                method = methods.get(0);
+            }
+        }
+        defaultMethod = method;
+
+        // mark the operations lists as unmodifiable, as they should not 
change during runtime
+        // to keep this code thread safe
+        operations = Collections.unmodifiableMap(operations);
+        operationsWithBody = Collections.unmodifiableList(operationsWithBody);
+        operationsWithNoBody = 
Collections.unmodifiableList(operationsWithNoBody);
+        operationsWithCustomAnnotation = 
Collections.unmodifiableList(operationsWithCustomAnnotation);
+        operationsWithHandlerAnnotation = 
Collections.unmodifiableList(operationsWithHandlerAnnotation);
+        methodMap = Collections.unmodifiableMap(methodMap);
+
+        // key must be instance based for custom/handler annotations
+        boolean instanceBased = !operationsWithCustomAnnotation.isEmpty() || 
!operationsWithHandlerAnnotation.isEmpty();
+        // do not cache Exchange based beans
+        instanceBased &= DefaultExchange.class != type;
+        if (instanceBased) {
+            // add new bean info to cache (instance based)
+            component.addBeanInfoToCache(key, this);
+        } else {
+            // add new bean info to cache (not instance based, favor key2 if 
possible)
+            BeanInfoCacheKey k = key2 != null ? key2 : key;
+            component.addBeanInfoToCache(k, this);
+        }
+    }
+
+    public Class<?> getType() {
+        return type;
+    }
+
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    public MethodInvocation createInvocation(Object pojo, Exchange exchange) {
+        return createInvocation(pojo, exchange, null);
+    }
+
+    public MethodInvocation createInvocation(Object pojo, Exchange exchange, 
String methodName)
+            throws AmbiguousMethodCallException, MethodNotFoundException {
+
+        MethodInfo methodInfo = null;
+
+        if (methodName != null) {
+
+            // do not use qualifier for name
+            String name = methodName;
+            if (methodName.contains("(")) {
+                name = StringHelper.before(methodName, "(");
+                // the must be a ending parenthesis
+                if (!methodName.endsWith(")")) {
+                    throw new IllegalArgumentException("Method should end with 
parenthesis, was " + methodName);
+                }
+                // and there must be an even number of parenthesis in the 
syntax
+                // (we can use betweenOuterPair as it return null if the 
syntax is invalid)
+                if (StringHelper.betweenOuterPair(methodName, '(', ')') == 
null) {
+                    throw new IllegalArgumentException("Method should have 
even pair of parenthesis, was " + methodName);
+                }
+            }
+            boolean emptyParameters = methodName.endsWith("()");
+
+            // special for getClass, as we want the user to be able to invoke 
this method
+            // for example to log the class type or the likes
+            if ("class".equals(name) || "getClass".equals(name)) {
+                methodInfo = createGetClassInvocation(pojo, exchange);
+                // special for length on an array type
+            } else if ("length".equals(name) && pojo.getClass().isArray()) {
+                methodInfo = createLengthInvocation(pojo, exchange);
+            } else {
+                List<MethodInfo> methods = getOperations(name);
+                if (methods != null && methods.size() == 1) {
+                    methodInfo = createSingleMethodInvocation(pojo, exchange, 
methods, emptyParameters, methodName);
+                } else if (methods != null) {
+                    methodInfo = evalMethods(pojo, exchange, methodName, 
emptyParameters, name, methods);
+                } else {
+                    // a specific method was given to invoke but not found
+                    throw new MethodNotFoundException(exchange, pojo, 
methodName);
+                }
+            }
+        }
+
+        if (methodInfo == null && methodMap.size() >= 2) {
+            // only try to choose if there is at least 2 methods
+            methodInfo = chooseMethod(pojo, exchange, null);
+        }
+        if (methodInfo == null) {
+            methodInfo = defaultMethod;
+        }
+        if (methodInfo != null) {
+            LOG.trace("Chosen method to invoke: {} on bean: {}", methodInfo, 
pojo);
+            return methodInfo.createMethodInvocation(pojo, 
methodInfo.hasParameters(), exchange);
+        }
+
+        LOG.debug("Cannot find suitable method to invoke on bean: {}", pojo);
+        return null;
+    }
+
+    private MethodInfo evalMethods(
+            Object pojo, Exchange exchange, String methodName, boolean 
emptyParameters, String name, List<MethodInfo> methods) {
+        MethodInfo methodInfo;
+        // there are more methods with that name so we cannot decide which to 
use
+
+        // but first let's try to choose a method and see if that complies 
with the name
+        // must use the method name which may have qualifiers
+        methodInfo = chooseMethod(pojo, exchange, methodName);
+
+        // validate that if we want an explicit no-arg method, then that's 
what we get
+        if (emptyParameters) {
+            if (methodInfo == null || methodInfo.hasParameters()) {
+                // we could not find a no-arg method with that name
+                throw new MethodNotFoundException(exchange, pojo, methodName, 
"(with no parameters)");
+            }
+        }
+
+        if (methodInfo == null || 
!name.equals(methodInfo.getMethod().getName())) {
+            throw new AmbiguousMethodCallException(exchange, methods);
+        }
+        return methodInfo;
+    }
+
+    private static MethodInfo createSingleMethodInvocation(
+            Object pojo, Exchange exchange, List<MethodInfo> methods, boolean 
emptyParameters, String methodName) {
+        MethodInfo methodInfo;
+        // only one method then choose it
+        methodInfo = methods.get(0);
+
+        // validate that if we want an explicit no-arg method, then that's 
what we get
+        if (emptyParameters && methodInfo.hasParameters()) {
+            throw new MethodNotFoundException(exchange, pojo, methodName, 
"(with no parameters)");
+        }
+        return methodInfo;
+    }
+
+    private static MethodInfo createLengthInvocation(Object pojo, Exchange 
exchange) {
+        MethodInfo methodInfo;
+        try {
+            // need to use arrayLength method from ObjectHelper as Camel's 
bean OGNL support is method invocation based
+            // and not for accessing fields. And hence we need to create a 
MethodInfo instance with a method to call
+            // and therefore use arrayLength from ObjectHelper to return the 
array length field.
+            Method method = 
org.apache.camel.util.ObjectHelper.class.getMethod("arrayLength", 
Object[].class);
+            ParameterInfo pi = new ParameterInfo(
+                    0, Object[].class, false, null, 
ExpressionBuilder.mandatoryBodyExpression(Object[].class, true));
+            List<ParameterInfo> lpi = new ArrayList<>(1);
+            lpi.add(pi);
+            methodInfo = new MethodInfo(exchange.getContext(), 
pojo.getClass(), method, lpi, lpi, false, false);
+            // Need to update the message body to be pojo for the invocation
+            exchange.getIn().setBody(pojo);
+        } catch (NoSuchMethodException e) {
+            throw new MethodNotFoundException(exchange, pojo, "getClass");
+        }
+        return methodInfo;
+    }
+
+    private static MethodInfo createGetClassInvocation(Object pojo, Exchange 
exchange) {
+        MethodInfo methodInfo;
+        try {
+            Method method = pojo.getClass().getMethod("getClass");
+            methodInfo = new MethodInfo(
+                    exchange.getContext(), pojo.getClass(), method, 
Collections.emptyList(),
+                    Collections.emptyList(), false, false);
+        } catch (NoSuchMethodException e) {
+            throw new MethodNotFoundException(exchange, pojo, "getClass");
+        }
+        return methodInfo;
+    }
+
+    /**
+     * Introspects the given class
+     *
+     * @param clazz the class
+     */
+    private void introspect(Class<?> clazz) {
+
+        // does the class have any public constructors?
+        publicConstructors = clazz.getConstructors().length > 0;
+        publicNoArgConstructors = 
org.apache.camel.util.ObjectHelper.hasDefaultPublicNoArgConstructor(clazz);
+
+        MethodsFilter methods = new MethodsFilter(getType());
+        introspect(clazz, methods);
+
+        // now introspect the methods and filter non valid methods
+        for (Method method : methods.asReadOnlyList()) {
+            boolean valid = isValidMethod(clazz, method);
+            LOG.trace("Method: {} is valid: {}", method, valid);
+            if (valid) {
+                introspect(clazz, method);
+            }
+        }
+    }
+
+    private void introspect(Class<?> clazz, MethodsFilter filteredMethods) {
+        // get the target clazz as it could potentially have been enhanced by
+        // CGLIB etc.
+        clazz = getTargetClass(clazz);
+        org.apache.camel.util.ObjectHelper.notNull(clazz, "clazz", this);
+
+        LOG.trace("Introspecting class: {}", clazz);
+
+        for (Method m : clazz.getDeclaredMethods()) {
+            filteredMethods.filterMethod(m);
+        }
+
+        Class<?> superClass = clazz.getSuperclass();
+        if (superClass != null && !superClass.equals(Object.class)) {
+            introspect(superClass, filteredMethods);
+        }
+        for (Class<?> superInterface : clazz.getInterfaces()) {
+            introspect(superInterface, filteredMethods);
+        }
+    }
+
+    /**
+     * Introspects the given method
+     *
+     * @param clazz  the class
+     * @param method the method
+     */
+    private void introspect(Class<?> clazz, Method method) {
+        LOG.trace("Introspecting class: {}, method: {}", clazz, method);
+        String opName = method.getName();
+
+        MethodInfo methodInfo = createMethodInfo(clazz, method);
+
+        // Foster the use of a potentially already registered most specific 
override
+        MethodInfo existingMethodInfo = findMostSpecificOverride(methodInfo);
+        if (existingMethodInfo != null) {
+            LOG.trace("This method is already overridden in a subclass, so the 
method from the sub class is preferred: {}",
+                    existingMethodInfo);
+            return;
+        }
+
+        LOG.trace("Adding operation: {} for method: {}", opName, methodInfo);
+
+        List<MethodInfo> existing = getOperations(opName);
+        if (existing != null) {
+            // we have an overloaded method so add the method info to the same 
key
+            existing.add(methodInfo);
+        } else {
+            // its a new method we have not seen before so wrap it in a list 
and add it
+            List<MethodInfo> methods = new ArrayList<>();
+            methods.add(methodInfo);
+            operations.put(opName, methods);
+        }
+
+        if (methodInfo.hasCustomAnnotation()) {
+            operationsWithCustomAnnotation.add(methodInfo);
+        } else if (methodInfo.hasBodyParameter()) {
+            operationsWithBody.add(methodInfo);
+        } else {
+            operationsWithNoBody.add(methodInfo);
+        }
+
+        if (methodInfo.hasHandlerAnnotation()) {
+            operationsWithHandlerAnnotation.add(methodInfo);
+        }
+
+        // must add to method map last otherwise we break stuff
+        methodMap.put(method, methodInfo);
+
+    }
+
+    /**
+     * Returns the {@link MethodInfo} for the given method if it exists or 
null if there is no metadata available for
+     * the given method
+     */
+    public MethodInfo getMethodInfo(Method method) {
+        MethodInfo answer = methodMap.get(method);
+        if (answer == null) {
+            // maybe the method overrides, and the method map keeps info of 
the source override we can use
+            for (Map.Entry<Method, MethodInfo> methodEntry : 
methodMap.entrySet()) {
+                Method source = methodEntry.getKey();
+                if (isOverridingMethod(source, method)) {
+                    answer = methodEntry.getValue();
+                    break;
+                }
+            }
+        }
+
+        if (answer == null) {
+            // maybe the method is defined on a base class?
+            if (type != Object.class) {
+                Class<?> superclass = type.getSuperclass();
+                if (superclass != null && superclass != Object.class) {
+                    BeanInfo superBeanInfo = new BeanInfo(camelContext, 
superclass, strategy, component);
+                    return superBeanInfo.getMethodInfo(method);
+                }
+            }
+        }
+        return answer;
+    }
+
+    protected MethodInfo createMethodInfo(Class<?> clazz, Method method) {
+        Class<?>[] parameterTypes = method.getParameterTypes();
+        List<Annotation>[] parametersAnnotations = 
collectParameterAnnotations(clazz, method);
+
+        List<ParameterInfo> parameters = new ArrayList<>();
+        List<ParameterInfo> bodyParameters = new ArrayList<>();
+
+        boolean hasCustomAnnotation = false;
+        boolean hasHandlerAnnotation = 
org.apache.camel.util.ObjectHelper.hasAnnotation(method.getAnnotations(), 
Handler.class);
+
+        int size = parameterTypes.length;
+
+        if (LOG.isTraceEnabled()) {
+            LOG.trace("Creating MethodInfo for class: {} method: {} having {} 
parameters", clazz, method, size);
+        }
+
+        for (int i = 0; i < size; i++) {
+            Class<?> parameterType = parameterTypes[i];
+            Annotation[] parameterAnnotations
+                    = parametersAnnotations[i].toArray(new Annotation[0]);
+            Expression expression = createParameterUnmarshalExpression(method, 
parameterType, parameterAnnotations);
+            if (expression == null) {
+                expression = 
strategy.getDefaultParameterTypeExpression(parameterType);
+            }
+            // this is not entirely correct as the parameter may be a default 
parameter type and not a custom annotation
+            // but we need to keep this logic for backwards compatability
+            hasCustomAnnotation |= expression != null;
+
+            // whether this parameter is vararg which must be last parameter
+            boolean varargs = method.isVarArgs() && i == size - 1;
+
+            ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, 
varargs, parameterAnnotations, expression);
+            LOG.trace("Parameter #{}: {}", i, parameterInfo);
+            parameters.add(parameterInfo);
+            if (expression == null) {
+                boolean bodyAnnotation = 
org.apache.camel.util.ObjectHelper.hasAnnotation(parameterAnnotations, 
Body.class);
+                LOG.trace("Parameter #{} has @Body annotation: {}", i, 
bodyAnnotation);
+                hasCustomAnnotation |= bodyAnnotation;
+                if (bodyParameters.isEmpty()) {
+                    // okay we have not yet set the body parameter and we have 
found
+                    // the candidate now to use as body parameter
+                    if (Exchange.class.isAssignableFrom(parameterType)) {
+                        // use exchange
+                        expression = ExpressionBuilder.exchangeExpression();
+                    } else {
+                        // assume it's the body and it must be mandatory 
convertible to the parameter type
+                        // but we allow null bodies in case the message really 
contains a null body
+                        expression = 
ExpressionBuilder.mandatoryBodyExpression(parameterType, true);
+                    }
+                    LOG.trace("Parameter #{} is the body parameter using 
expression {}", i, expression);
+                    parameterInfo.setExpression(expression);
+                    bodyParameters.add(parameterInfo);
+                } else {
+                    // will ignore the expression for parameter evaluation
+                }
+            }
+            LOG.trace("Parameter #{} has parameter info: {}", i, 
parameterInfo);
+        }
+
+        // now let's add the method to the repository
+        return new MethodInfo(
+                camelContext, clazz, method, parameters, bodyParameters, 
hasCustomAnnotation, hasHandlerAnnotation);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected List<Annotation>[] collectParameterAnnotations(Class<?> c, 
Method m) {
+        List<Annotation>[] annotations = new List[m.getParameterCount()];
+        for (int i = 0; i < annotations.length; i++) {
+            annotations[i] = new ArrayList<>();
+        }
+        collectParameterAnnotations(c, m, annotations);
+        return annotations;
+    }
+
+    protected void collectParameterAnnotations(Class<?> c, Method m, 
List<Annotation>[] a) {
+        // because we are only looking for camel annotations then skip all 
stuff from JDKs
+        if (c.getName().startsWith("java")) {
+            return;
+        }
+        try {
+            Annotation[][] pa = c.getDeclaredMethod(m.getName(), 
m.getParameterTypes()).getParameterAnnotations();
+            for (int i = 0; i < pa.length; i++) {
+                a[i].addAll(Arrays.asList(pa[i]));
+            }
+        } catch (NoSuchMethodException e) {
+            // ignore no method with signature of m declared on c
+        }
+        for (Class<?> i : c.getInterfaces()) {
+            collectParameterAnnotations(i, m, a);
+        }
+        if (!c.isInterface() && c.getSuperclass() != null && c.getSuperclass() 
!= Object.class) {
+            collectParameterAnnotations(c.getSuperclass(), m, a);
+        }
+    }
+
+    /**
+     * Choose one of the available methods to invoke if we can match the 
message body to the body parameter
+     *
+     * @param  pojo                         the bean to invoke a method on
+     * @param  exchange                     the message exchange
+     * @param  name                         an optional name of the method 
that must match, use <tt>null</tt> to
+     *                                      indicate all methods
+     * @return                              the method to invoke or null if no 
definitive method could be matched
+     * @throws AmbiguousMethodCallException is thrown if cannot choose method 
due to ambiguity
+     */
+    protected MethodInfo chooseMethod(Object pojo, Exchange exchange, String 
name) throws AmbiguousMethodCallException {
+        // @Handler should be select first
+        // then any single method that has a custom @annotation
+        // or any single method that has a match parameter type that matches 
the Exchange payload
+        // and last then try to select the best among the rest
+
+        // must use defensive copy, to avoid altering the shared lists
+        // and we want to remove unwanted operations from these local lists
+        List<MethodInfo> localOperationsWithBody = null;
+        if (!operationsWithBody.isEmpty()) {
+            localOperationsWithBody = new ArrayList<>(operationsWithBody);
+        }
+        List<MethodInfo> localOperationsWithNoBody = null;
+        if (!operationsWithNoBody.isEmpty()) {
+            localOperationsWithNoBody = new ArrayList<>(operationsWithNoBody);
+        }
+        List<MethodInfo> localOperationsWithCustomAnnotation = null;
+        if (!operationsWithCustomAnnotation.isEmpty()) {
+            localOperationsWithCustomAnnotation = new 
ArrayList<>(operationsWithCustomAnnotation);
+        }
+        List<MethodInfo> localOperationsWithHandlerAnnotation = null;
+        if (!operationsWithHandlerAnnotation.isEmpty()) {
+            localOperationsWithHandlerAnnotation = new 
ArrayList<>(operationsWithHandlerAnnotation);
+        }
+
+        // remove all abstract methods
+        if (localOperationsWithBody != null) {
+            removeAllAbstractMethods(localOperationsWithBody);
+        }
+        if (localOperationsWithNoBody != null) {
+            removeAllAbstractMethods(localOperationsWithNoBody);
+        }
+        if (localOperationsWithCustomAnnotation != null) {
+            removeAllAbstractMethods(localOperationsWithCustomAnnotation);
+        }
+        if (localOperationsWithHandlerAnnotation != null) {
+            removeAllAbstractMethods(localOperationsWithHandlerAnnotation);
+        }
+
+        if (name != null) {
+            // filter all lists to only include methods with this name
+            if (localOperationsWithHandlerAnnotation != null) {
+                removeNonMatchingMethods(localOperationsWithHandlerAnnotation, 
name);
+            }
+            if (localOperationsWithCustomAnnotation != null) {
+                removeNonMatchingMethods(localOperationsWithCustomAnnotation, 
name);
+            }
+            if (localOperationsWithBody != null) {
+                removeNonMatchingMethods(localOperationsWithBody, name);
+            }
+            if (localOperationsWithNoBody != null) {
+                removeNonMatchingMethods(localOperationsWithNoBody, name);
+            }
+        } else {
+            // remove all getter/setter as we do not want to consider these 
methods
+            if (localOperationsWithHandlerAnnotation != null) {
+                
removeAllSetterOrGetterMethods(localOperationsWithHandlerAnnotation);
+            }
+            if (localOperationsWithCustomAnnotation != null) {
+                
removeAllSetterOrGetterMethods(localOperationsWithCustomAnnotation);
+            }
+            if (localOperationsWithBody != null) {
+                removeAllSetterOrGetterMethods(localOperationsWithBody);
+            }
+            if (localOperationsWithNoBody != null) {
+                removeAllSetterOrGetterMethods(localOperationsWithNoBody);
+            }
+        }
+
+        if (localOperationsWithHandlerAnnotation != null && 
localOperationsWithHandlerAnnotation.size() > 1) {
+            // if we have more than 1 @Handler then its ambiguous
+            throw new AmbiguousMethodCallException(exchange, 
localOperationsWithHandlerAnnotation);
+        }
+
+        if (localOperationsWithHandlerAnnotation != null && 
localOperationsWithHandlerAnnotation.size() == 1) {
+            // methods with handler should be preferred
+            return localOperationsWithHandlerAnnotation.get(0);
+        } else if (localOperationsWithCustomAnnotation != null && 
localOperationsWithCustomAnnotation.size() == 1) {
+            // if there is one method with an annotation then use that one
+            return localOperationsWithCustomAnnotation.get(0);
+        }
+
+        // named method and with no parameters
+        boolean noParameters = name != null && name.endsWith("()");
+        if (noParameters && localOperationsWithNoBody != null && 
localOperationsWithNoBody.size() == 1) {
+            // if there was a method name configured and it has no parameters, 
then use the method with no body (eg no parameters)
+            return localOperationsWithNoBody.get(0);
+        } else if (!noParameters && localOperationsWithBody != null && 
localOperationsWithBody.size() == 1
+                && localOperationsWithCustomAnnotation == null) {
+            // if there is one method with body then use that one
+            return localOperationsWithBody.get(0);
+        }
+
+        if (localOperationsWithBody != null || 
localOperationsWithCustomAnnotation != null) {
+            Collection<MethodInfo> possibleOperations = new ArrayList<>();
+            if (localOperationsWithBody != null) {
+                possibleOperations.addAll(localOperationsWithBody);
+            }
+            if (localOperationsWithCustomAnnotation != null) {
+                possibleOperations.addAll(localOperationsWithCustomAnnotation);
+            }
+
+            if (!possibleOperations.isEmpty()) {
+                MethodInfo answer = null;
+
+                if (name != null) {
+                    // do we have hardcoded parameters values provided from 
the method name then use that for matching
+                    String parameters = StringHelper.between(name, "(", ")");
+                    if (parameters != null) {
+                        // special as we have hardcoded parameters, so we need 
to choose method that matches those parameters the best
+                        LOG.trace("Choosing best matching method matching 
parameters: {}", parameters);
+                        answer = chooseMethodWithMatchingParameters(exchange, 
parameters, possibleOperations);
+                    }
+                }
+                if (answer == null) {
+                    // multiple possible operations so find the best suited if 
possible
+                    answer = chooseMethodWithMatchingBody(exchange, 
possibleOperations, localOperationsWithCustomAnnotation);
+                }
+                if (answer == null && possibleOperations.size() > 1) {
+                    answer = getSingleCovariantMethod(possibleOperations);
+                }
+
+                if (answer == null) {
+                    throw new AmbiguousMethodCallException(exchange, 
possibleOperations);
+                } else {
+                    return answer;
+                }
+            }
+        }
+
+        // not possible to determine
+        return null;
+    }
+
+    private MethodInfo chooseMethodWithMatchingParameters(
+            Exchange exchange, String parameters, Collection<MethodInfo> 
operationList)
+            throws AmbiguousMethodCallException {
+        // we have hardcoded parameters so need to match that with the given 
operations
+        int count = 0;
+        for (String o : ObjectHelper.createIterable(parameters)) {
+            count++;
+        }
+
+        List<MethodInfo> operations = new ArrayList<>();
+        for (MethodInfo info : operationList) {
+            if (info.getParameters().size() == count) {
+                operations.add(info);
+            }
+        }
+
+        if (operations.isEmpty()) {
+            return null;
+        } else if (operations.size() == 1) {
+            return operations.get(0);
+        }
+
+        // okay we still got multiple operations, so need to match the best one
+        List<MethodInfo> candidates = new ArrayList<>();
+        // look for best method without any type conversion
+        MethodInfo fallbackCandidate = chooseBestPossibleMethod(exchange, 
parameters, false, operations, candidates);
+        if (fallbackCandidate == null && candidates.isEmpty()) {
+            // okay then look again for best method with type conversion
+            fallbackCandidate = chooseBestPossibleMethod(exchange, parameters, 
true, operations, candidates);
+        }
+        if (candidates.size() > 1) {
+            MethodInfo answer = getSingleCovariantMethod(candidates);
+            if (answer != null) {
+                return answer;
+            }
+        }
+        return candidates.size() == 1 ? candidates.get(0) : fallbackCandidate;
+    }
+
+    private MethodInfo chooseBestPossibleMethod(
+            Exchange exchange, String parameters, boolean allowConversion,
+            List<MethodInfo> operations, List<MethodInfo> candidates) {
+        MethodInfo fallbackCandidate = null;
+
+        for (MethodInfo info : operations) {
+            Iterator<?> it = ObjectHelper.createIterator(parameters, ",", 
false);
+            int index = 0;
+            boolean matches = true;
+            while (it.hasNext()) {
+                String parameter = (String) it.next();
+                if (parameter != null) {
+                    // must trim
+                    parameter = parameter.trim();
+                }
+
+                Class<?> parameterType = 
BeanHelper.getValidParameterType(parameter);
+                Class<?> expectedType = 
info.getParameters().get(index).getType();
+
+                if (parameterType != null && expectedType != null) {
+
+                    // if its a simple language then we need to evaluate the 
expression
+                    // so we have the result and can find out what type the 
parameter actually is
+                    if (StringHelper.hasStartToken(parameter, "simple")) {
+                        LOG.trace(
+                                "Evaluating simple expression for parameter 
#{}: {} to determine the class type of the parameter",
+                                index, parameter);
+                        Object out = 
getCamelContext().resolveLanguage("simple").createExpression(parameter).evaluate(exchange,
+                                Object.class);
+                        if (out != null) {
+                            parameterType = out.getClass();
+                        }
+                    }
+
+                    // skip java.lang.Object type, when we have multiple 
possible methods we want to avoid it if possible
+                    if (Object.class.equals(expectedType)) {
+                        fallbackCandidate = info;
+                        matches = false;
+                        break;
+                    }
+
+                    boolean matchingTypes = 
isParameterMatchingType(parameterType, expectedType);
+                    if (!matchingTypes && allowConversion) {
+                        matchingTypes
+                                = 
getCamelContext().getTypeConverterRegistry().lookup(expectedType, 
parameterType) != null;
+                    }
+                    if (!matchingTypes) {
+                        matches = false;
+                        break;
+                    }
+                }
+
+                index++;
+            }
+
+            if (matches) {
+                candidates.add(info);
+            }
+        }
+        return fallbackCandidate;
+    }
+
+    private boolean isParameterMatchingType(Class<?> parameterType, Class<?> 
expectedType) {
+        if (Number.class.equals(parameterType)) {
+            // number should match long/int/etc.
+            if (Integer.class.isAssignableFrom(expectedType) || 
Long.class.isAssignableFrom(expectedType)
+                    || int.class.isAssignableFrom(expectedType) || 
long.class.isAssignableFrom(expectedType)) {
+                return true;
+            }
+        }
+        if (Boolean.class.equals(parameterType)) {
+            // boolean should match both Boolean and boolean
+            if (Boolean.class.isAssignableFrom(expectedType) || 
boolean.class.isAssignableFrom(expectedType)) {
+                return true;
+            }
+        }
+        return expectedType.isAssignableFrom(parameterType);
+        //        return parameterType.isAssignableFrom(expectedType);
+    }
+
+    private MethodInfo getSingleCovariantMethod(Collection<MethodInfo> 
candidates) {
+        // if all the candidates are actually covariant, it doesn't matter 
which one we call
+        MethodInfo firstCandidate = candidates.iterator().next();
+        for (MethodInfo candidate : candidates) {
+            if (!firstCandidate.isCovariantWith(candidate)) {
+                return null;
+            }
+        }
+        return firstCandidate;
+    }
+
+    private MethodInfo chooseMethodWithMatchingBody(
+            Exchange exchange, Collection<MethodInfo> operationList,
+            List<MethodInfo> operationsWithCustomAnnotation)
+            throws AmbiguousMethodCallException {
+        // see if we can find a method whose body param type matches the 
message body
+        Message in = exchange.getIn();
+        Object body = in.getBody();
+        if (body != null) {
+            Class<?> bodyType = body.getClass();
+            if (LOG.isTraceEnabled()) {
+                LOG.trace("Matching for method with a single parameter that 
matches type: {}", bodyType.getCanonicalName());
+            }
+
+            List<MethodInfo> possibles = new ArrayList<>();
+            List<MethodInfo> possiblesWithException = null;
+            for (MethodInfo methodInfo : operationList) {
+                // test for MEP pattern matching
+                boolean out = exchange.getPattern().isOutCapable();
+                if (out && methodInfo.isReturnTypeVoid()) {
+                    // skip this method as the MEP is Out so the method must 
return something
+                    continue;
+                }
+
+                // try to match the arguments
+                if (methodInfo.bodyParameterMatches(bodyType)) {
+                    LOG.trace("Found a possible method: {}", methodInfo);
+                    if (methodInfo.hasExceptionParameter()) {
+                        // methods with accepts exceptions
+                        if (possiblesWithException == null) {
+                            possiblesWithException = new ArrayList<>();
+                        }
+                        possiblesWithException.add(methodInfo);
+                    } else {
+                        // regular methods with no exceptions
+                        possibles.add(methodInfo);
+                    }
+                }
+            }
+
+            // find best suited method to use
+            return chooseBestPossibleMethodInfo(exchange, operationList, body, 
possibles, possiblesWithException,
+                    operationsWithCustomAnnotation);
+        }
+
+        // no match so return null
+        return null;
+    }
+
+    private MethodInfo chooseBestPossibleMethodInfo(
+            Exchange exchange, Collection<MethodInfo> operationList, Object 
body,
+            List<MethodInfo> possibles, List<MethodInfo> 
possiblesWithException,
+            List<MethodInfo> possibleWithCustomAnnotation)
+            throws AmbiguousMethodCallException {
+
+        Exception exception = 
ExpressionBuilder.exchangeExceptionExpression().evaluate(exchange, 
Exception.class);
+        if (exception != null && possiblesWithException != null && 
possiblesWithException.size() == 1) {
+            LOG.trace("Exchange has exception set so we prefer method that 
also has exception as parameter");
+            // prefer the method that accepts exception in case we have an 
exception also
+            return possiblesWithException.get(0);
+        } else if (possibles.size() == 1) {
+            return possibles.get(0);
+        } else if (possibles.isEmpty()) {
+            LOG.trace("No possible methods so now trying to convert body to 
parameter types");
+
+            // let's try converting
+            Object newBody = null;
+            MethodInfo matched = null;
+            int matchCounter = 0;
+            for (MethodInfo methodInfo : operationList) {
+                if (methodInfo.getBodyParameterType() != null) {
+                    if (methodInfo.getBodyParameterType().isInstance(body)) {
+                        return methodInfo;
+                    }
+
+                    // we should only try to convert, as we are looking for 
best match
+                    Object value = 
exchange.getContext().getTypeConverter().tryConvertTo(methodInfo.getBodyParameterType(),
+                            exchange, body);
+                    if (value != null) {
+                        if (LOG.isTraceEnabled()) {
+                            LOG.trace("Converted body from: {} to: {}",
+                                    body.getClass().getCanonicalName(), 
methodInfo.getBodyParameterType().getCanonicalName());
+                        }
+                        matchCounter++;
+                        newBody = value;
+                        matched = methodInfo;
+                    }
+                }
+            }
+            if (matchCounter > 1) {
+                throw new AmbiguousMethodCallException(exchange, 
Arrays.asList(matched, matched));
+            }
+            if (matched != null) {
+                LOG.trace("Setting converted body: {}", body);
+                Message in = exchange.getIn();
+                in.setBody(newBody);
+                return matched;
+            }
+        } else {
+            // if we only have a single method with custom annotations, let's 
use that one
+            if (possibleWithCustomAnnotation != null && 
possibleWithCustomAnnotation.size() == 1) {
+                MethodInfo answer = possibleWithCustomAnnotation.get(0);
+                LOG.trace("There are only one method with annotations so we 
choose it: {}", answer);
+                return answer;
+            }
+            // try to choose among multiple methods with annotations
+            MethodInfo chosen = chooseMethodWithCustomAnnotations(possibles);
+            if (chosen != null) {
+                return chosen;
+            }
+            // just make sure the methods aren't all actually the same
+            chosen = getSingleCovariantMethod(possibles);
+            if (chosen != null) {
+                return chosen;
+            }
+            throw new AmbiguousMethodCallException(exchange, possibles);
+        }
+
+        // cannot find a good method to use
+        return null;
+    }
+
+    /**
+     * Validates whether the given method is a valid candidate for Camel Bean 
Binding.
+     *
+     * @param  clazz  the class
+     * @param  method the method
+     * @return        true if valid, false to skip the method
+     */
+    protected boolean isValidMethod(Class<?> clazz, Method method) {
+        // method name must not be in the excluded list
+        String name = method.getName();
+        for (String s : EXCLUDED_METHOD_NAMES) {
+            if (name.equals(s)) {
+                return false;
+            }
+        }
+
+        // special for Object where clone is not allowed to be called directly
+        if (Object.class == clazz && "clone".equals(name)) {
+            return false;
+        }
+
+        // must not be a private method
+        boolean privateMethod = Modifier.isPrivate(method.getModifiers());
+        if (privateMethod) {
+            return false;
+        }
+
+        // return type must not be an Exchange and it should not be a bridge 
method
+        if (Exchange.class.isAssignableFrom(method.getReturnType()) || 
method.isBridge()) {
+            return false;
+        }
+
+        // must not be a method added by Mockito (CGLIB or Byte Buddy)
+        if (name.contains(CGLIB_METHOD_MARKER) || 
name.contains(BYTE_BUDDY_METHOD_MARKER)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Gets the most specific override of a given method, if any. Ignores 
overrides from hidden and synthetic classes.
+     * Indeed, overrides may have already been found while inspecting sub 
classes. Or the given method could override an
+     * interface extra method.
+     * <p>
+     * Hidden synthetic classes (such as lambdas and method references) are 
always excluded from override detection to
+     * preserve annotation discovery on their declaring interface. Non-hidden 
synthetic classes that implement an
+     * interface (such as OSGi Blueprint proxies) are still considered for 
override detection to avoid
+     * {@link AmbiguousMethodCallException}.
+     *
+     * @param  proposedMethodInfo the method for which a more specific 
override is searched
+     * @return                    The already registered most specific 
override if any, otherwise <code>null</code>
+     */
+    private MethodInfo findMostSpecificOverride(MethodInfo proposedMethodInfo) 
{
+        for (MethodInfo alreadyRegisteredMethodInfo : methodMap.values()) {
+            Method alreadyRegisteredMethod = 
alreadyRegisteredMethodInfo.getMethod();
+            Method proposedMethod = proposedMethodInfo.getMethod();
+
+            if (!alreadyRegisteredMethod.getDeclaringClass().isSynthetic()
+                    && isOverridingMethod(proposedMethod, 
alreadyRegisteredMethod)) {
+                return alreadyRegisteredMethodInfo;
+            } else if 
(alreadyRegisteredMethod.getDeclaringClass().isSynthetic()
+                    && !alreadyRegisteredMethod.getDeclaringClass().isHidden()
+                    && proposedMethod.getDeclaringClass().isInterface()
+                    && isOverridingMethod(proposedMethod, 
alreadyRegisteredMethod)) {
+                // Non-hidden synthetic classes implementing interfaces (e.g. 
OSGi Blueprint proxies)
+                // should still be detected as overrides to avoid 
AmbiguousMethodCallException
+                return alreadyRegisteredMethodInfo;
+            } else if (isOverridingMethod(alreadyRegisteredMethod, 
proposedMethod)) {
+                return proposedMethodInfo;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Wrapper loosely checking the bean type for overrides
+     *
+     * @see org.apache.camel.util.ObjectHelper#isOverridingMethod(Class, 
Method, Method, boolean)
+     */
+    private boolean isOverridingMethod(Method source, Method target) {
+        return 
org.apache.camel.util.ObjectHelper.isOverridingMethod(getType(), source, 
target, false);
+    }
+
+    private MethodInfo 
chooseMethodWithCustomAnnotations(Collection<MethodInfo> possibles) {
+        // if we have only one method with custom annotations let's choose that
+        MethodInfo chosen = null;
+        for (MethodInfo possible : possibles) {
+            if (possible.hasCustomAnnotation()) {
+                if (chosen != null) {
+                    chosen = null;
+                    break;
+                } else {
+                    chosen = possible;
+                }
+            }
+        }
+        return chosen;
+    }
+
+    /**
+     * Creates an expression for the given parameter type if the parameter can 
be mapped automatically or null if the
+     * parameter cannot be mapped due to insufficient annotations or not 
fitting with the default type conventions.
+     */
+    private Expression createParameterUnmarshalExpression(
+            Method method,
+            Class<?> parameterType, Annotation[] parameterAnnotation) {
+
+        // look for a parameter annotation that converts into an expression
+        for (Annotation annotation : parameterAnnotation) {
+            Expression answer = 
createParameterUnmarshalExpressionForAnnotation(method, parameterType, 
annotation);
+            if (answer != null) {
+                return answer;
+            }
+        }
+        return null;
+    }
+
+    private Expression createParameterUnmarshalExpressionForAnnotation(
+            Method method,
+            Class<?> parameterType, Annotation annotation) {
+        if (annotation instanceof ExchangeProperty propertyAnnotation) {
+            return 
ExpressionBuilder.exchangePropertyExpression(propertyAnnotation.value());
+        } else if (annotation instanceof ExchangeProperties) {
+            return ExpressionBuilder.exchangePropertiesExpression();
+        } else if (annotation instanceof Header headerAnnotation) {
+            return 
ExpressionBuilder.headerExpression(headerAnnotation.value());
+        } else if (annotation instanceof Headers) {
+            return ExpressionBuilder.headersExpression();
+        } else if (annotation instanceof Variable variableAnnotation) {
+            return 
ExpressionBuilder.variableExpression(variableAnnotation.value());
+        } else if (annotation instanceof Variables) {
+            return ExpressionBuilder.variablesExpression();
+        } else if (annotation instanceof ExchangeException) {
+            return 
ExpressionBuilder.exchangeExceptionExpression(CastUtils.cast(parameterType, 
Exception.class));
+        } else if (annotation instanceof PropertyInject propertyAnnotation) {
+            Expression inject = 
ExpressionBuilder.propertiesComponentExpression(propertyAnnotation.value(),
+                    propertyAnnotation.defaultValue());
+            return ExpressionBuilder.convertToExpression(inject, 
parameterType);
+        } else {
+            LanguageAnnotation languageAnnotation = 
annotation.annotationType().getAnnotation(LanguageAnnotation.class);
+            if (languageAnnotation != null) {
+                Class<?> type = languageAnnotation.factory();
+                if (type == Object.class) {
+                    // use the default factory
+                    type = DefaultAnnotationExpressionFactory.class;
+                }
+                Object object = camelContext.getInjector().newInstance(type);
+                if (object instanceof AnnotationExpressionFactory 
expressionFactory) {
+                    return expressionFactory.createExpression(camelContext, 
annotation, languageAnnotation, parameterType);
+                } else {
+                    LOG.warn(
+                            "Ignoring bad annotation: {} on method: {} which 
declares a factory {} which does not implement {}",
+                            languageAnnotation, method, type.getName(), 
AnnotationExpressionFactory.class.getName());
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private static void removeAllSetterOrGetterMethods(List<MethodInfo> 
methods) {
+        Iterator<MethodInfo> it = methods.iterator();
+        while (it.hasNext()) {
+            MethodInfo info = it.next();
+            if (isGetter(info.getMethod())) {
+                // skip getters
+                it.remove();
+            } else if (isSetter(info.getMethod())) {
+                // skip setters
+                it.remove();
+            }
+        }
+    }
+
+    private void removeNonMatchingMethods(List<MethodInfo> methods, String 
name) {
+        // method does not match so remove it
+        methods.removeIf(info -> !matchMethod(info.getMethod(), name));
+    }
+
+    private void removeAllAbstractMethods(List<MethodInfo> methods) {
+        Iterator<MethodInfo> it = methods.iterator();
+        while (it.hasNext()) {
+            MethodInfo info = it.next();
+            // if the class is an interface then keep the method
+            boolean isFromInterface = 
Modifier.isInterface(info.getMethod().getDeclaringClass().getModifiers());
+            if (!isFromInterface && 
Modifier.isAbstract(info.getMethod().getModifiers())) {
+                // we cannot invoke an abstract method
+                it.remove();
+            }
+        }
+    }
+
+    private boolean matchMethod(Method method, String methodName) {
+        if (methodName == null) {
+            return true;
+        }
+
+        if (methodName.contains("(") && !methodName.endsWith(")")) {
+            throw new IllegalArgumentException("Name must have both starting 
and ending parenthesis, was: " + methodName);
+        }
+
+        // do not use qualifier for name matching
+        String name = methodName;
+        if (name.contains("(")) {
+            name = StringHelper.before(name, "(");
+        }
+
+        // must match name
+        if (name != null && !name.equals(method.getName())) {
+            return false;
+        }
+
+        // is it a method with no parameters
+        boolean noParameters = methodName.endsWith("()");
+        if (noParameters) {
+            return method.getParameterCount() == 0;
+        }
+
+        // match qualifier types which is used to select among overloaded 
methods
+        String types = StringHelper.betweenOuterPair(methodName, '(', ')');
+        if (org.apache.camel.util.ObjectHelper.isNotEmpty(types)) {
+            // we must qualify based on types to match method
+            String[] parameters = StringQuoteHelper.splitSafeQuote(types, ',', 
true, true);
+            Class<?>[] parameterTypes = null;
+            Iterator<?> it = ObjectHelper.createIterator(parameters);
+            for (int i = 0; i < method.getParameterCount(); i++) {
+                if (it.hasNext()) {
+                    if (parameterTypes == null) {
+                        parameterTypes = method.getParameterTypes();
+                    }
+                    Class<?> parameterType = parameterTypes[i];
+
+                    String qualifyType = (String) it.next();
+                    if 
(org.apache.camel.util.ObjectHelper.isEmpty(qualifyType)) {
+                        continue;
+                    }
+                    // trim the type
+                    qualifyType = qualifyType.trim();
+                    String value = qualifyType;
+                    int pos1 = qualifyType.indexOf(' ');
+                    int pos2 = qualifyType.indexOf(".class");
+                    if (pos1 != -1 && pos2 != -1 && pos1 > pos2) {
+                        // a parameter can include type in the syntax to help 
with choosing correct method
+                        // therefore we need to check if type is provided in 
syntax (name.class value, name2.class value2, ...)
+                        value = qualifyType.substring(pos1);
+                        value = value.trim();
+                        qualifyType = qualifyType.substring(0, pos1);
+                        qualifyType = qualifyType.trim();
+                    }
+
+                    if ("*".equals(qualifyType)) {
+                        // * is a wildcard so we accept and match that 
parameter type
+                        continue;
+                    }
+
+                    // if qualify type indeed is a class, then it must be 
assignable with the parameter type
+                    Boolean assignable = 
BeanHelper.isAssignableToExpectedType(getCamelContext().getClassResolver(),
+                            qualifyType, parameterType);
+                    // the method will return null if the qualifyType is not a 
class
+                    if (assignable != null && !assignable) {
+                        return false;
+                    }
+
+                    if (!qualifyType.endsWith(".class")
+                            && !BeanHelper.isValidParameterValue(value)) {
+                        // its a parameter value, so continue to next parameter
+                        // as we should only check for FQN/type parameters
+                        return false;
+                    }
+
+                } else {
+                    // there method has more parameters than was specified in 
the method name qualifiers
+                    return false;
+                }
+            }
+
+            // if the method has no more types then we can only regard it as 
matched
+            // if there are no more qualifiers
+            if (it.hasNext()) {
+                return false;
+            }
+        }
+
+        // the method matched
+        return true;
+    }
+
+    private static Class<?> getTargetClass(Class<?> clazz) {
+        if (clazz != null
+                && (clazz.getName().contains(CGLIB_CLASS_SEPARATOR) || 
clazz.getName().endsWith(CLIENT_PROXY_SUFFIX)
+                        || clazz.getName().endsWith(SUBCLASS_SUFFIX))) {
+            Class<?> superClass = clazz.getSuperclass();
+            if (superClass != null && !Object.class.equals(superClass)) {
+                return superClass;
+            }
+        }
+        return clazz;
+    }
+
+    /**
+     * Do we have a method with the given name?
+     * <p/>
+     * Shorthand method names for getters is supported, so you can pass in eg 
'name' and Camel will can find the real
+     * 'getName' method instead.
+     *
+     * @param  methodName the method name
+     * @return            <tt>true</tt> if we have such a method.
+     */
+    public boolean hasMethod(String methodName) {
+        return getOperations(methodName) != null;
+    }
+
+    /**
+     * Do we have a static method with the given name.
+     * <p/>
+     * Shorthand method names for getters is supported, so you can pass in eg 
'name' and Camel will can find the real
+     * 'getName' method instead.
+     *
+     * @param  methodName the method name
+     * @return            <tt>true</tt> if we have such a static method.
+     */
+    public boolean hasStaticMethod(String methodName) {
+        List<MethodInfo> methods = getOperations(methodName);
+        if (methods == null || methods.isEmpty()) {
+            return false;
+        }
+        for (MethodInfo method : methods) {
+            if (method.isStaticMethod()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns whether the bean class has any public constructors.
+     */
+    public boolean hasPublicConstructors() {
+        return publicConstructors;
+    }
+
+    /**
+     * Returns whether the bean class has any public no-arg constructors.
+     */
+    public boolean hasPublicNoArgConstructors() {
+        return publicNoArgConstructors;
+    }
+
+    /**
+     * Gets the list of methods sorted by A..Z method name.
+     *
+     * @return the methods.
+     */
+    public List<MethodInfo> getMethods() {
+        if (operations.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<MethodInfo> methods = new ArrayList<>();
+        for (Collection<MethodInfo> col : operations.values()) {
+            methods.addAll(col);
+        }
+
+        if (methods.size() > 1) {
+            // sort the methods by name A..Z
+            methods.sort(Comparator.comparing(o -> o.getMethod().getName()));
+        }
+        return methods;
+    }
+
+    /**
+     * Does any of the methods have a Canel @Handler annotation.
+     */
+    public boolean hasAnyMethodHandlerAnnotation() {
+        return !operationsWithHandlerAnnotation.isEmpty();
+    }
+
+    /**
+     * Get the operation(s) with the given name. We can have multiple when 
methods is overloaded.
+     * <p/>
+     * Shorthand method names for getters is supported, so you can pass in eg 
'name' and Camel will can find the real
+     * 'getName' method instead.
+     *
+     * @param  methodName the method name
+     * @return            the found method, or <tt>null</tt> if not found
+     */
+    private List<MethodInfo> getOperations(String methodName) {
+        // do not use qualifier for name
+        if (methodName.contains("(")) {
+            methodName = StringHelper.before(methodName, "(");
+        }
+
+        List<MethodInfo> answer = operations.get(methodName);
+        if (answer != null) {
+            return answer;
+        }
+
+        // now try all getters to see if any of those matched the methodName
+        for (Method method : methodMap.keySet()) {
+            if (isGetter(method)) {
+                String shorthandMethodName = getGetterShorthandName(method);
+                // if the two names matches then see if we can find it using 
that name
+                if (methodName != null && 
methodName.equals(shorthandMethodName)) {
+                    return operations.get(method.getName());
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public static boolean isGetter(Method method) {
+        String name = method.getName();
+        Class<?> type = method.getReturnType();
+        int parameterCount = method.getParameterCount();
+
+        // is it a getXXX method
+        if (name.startsWith("get") && name.length() >= 4 && 
Character.isUpperCase(name.charAt(3))) {
+            return parameterCount == 0 && !type.equals(Void.TYPE);
+        }
+
+        // special for isXXX boolean
+        if (name.startsWith("is") && name.length() >= 3 && 
Character.isUpperCase(name.charAt(2))) {
+            return parameterCount == 0 && 
type.getSimpleName().equalsIgnoreCase("boolean");
+        }
+
+        return false;
+    }
+
+    public static boolean isSetter(Method method) {
+        String name = method.getName();
+        Class<?> type = method.getReturnType();
+        int parameterCount = method.getParameterCount();
+
+        // is it a setXXX method
+        boolean validName = name.startsWith("set") && name.length() >= 4 && 
Character.isUpperCase(name.charAt(3));
+        if (validName && parameterCount == 1) {
+            // a setXXX can also be a builder pattern so check for its return 
type is itself
+            return type.equals(Void.TYPE);
+        }
+
+        return false;
+    }
+
+    public static String getGetterShorthandName(Method method) {
+        if (!isGetter(method)) {
+            return method.getName();
+        }
+
+        String name = method.getName();
+        if (name.startsWith("get")) {
+            name = name.substring(3);
+            name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + 
name.substring(1);
+        } else if (name.startsWith("is")) {
+            name = name.substring(2);
+            name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + 
name.substring(1);
+        }
+
+        return name;
+    }
+
+}
diff --git 
a/components/camel-bean/src/test/java/org/apache/camel/component/bean/BeanInfoSyntheticProxyTest.java
 
b/components/camel-bean/src/test/java/org/apache/camel/component/bean/BeanInfoSyntheticProxyTest.java
new file mode 100644
index 000000000..61c1e06c2
--- /dev/null
+++ 
b/components/camel-bean/src/test/java/org/apache/camel/component/bean/BeanInfoSyntheticProxyTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.camel.component.bean;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.description.modifier.SyntheticState;
+import net.bytebuddy.description.modifier.Visibility;
+import net.bytebuddy.implementation.InvocationHandlerAdapter;
+import net.bytebuddy.matcher.ElementMatchers;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Handler;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests that BeanInfo correctly handles synthetic proxy classes implementing 
interfaces,
+ * such as OSGi Blueprint service reference proxies.
+ *
+ * When a Blueprint {@code <reference>} injects an OSGi service, the proxy 
class is synthetic.
+ * BeanInfo must detect that the proxy's method overrides the interface method 
to avoid
+ * registering duplicate operations for the same method name.
+ */
+public class BeanInfoSyntheticProxyTest {
+
+    private CamelContext context;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        context = new DefaultCamelContext();
+        context.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        if (context != null) {
+            context.stop();
+        }
+    }
+
+    /**
+     * Simulates an OSGi Blueprint proxy: a synthetic class that implements a 
service interface.
+     * Without the fix, BeanInfo registers both the interface's echo() and the 
proxy's echo()
+     * as separate operations, resulting in duplicate entries for the same 
method name.
+     */
+    @Test
+    void testSyntheticInterfaceProxyMethodResolution() throws Exception {
+        Object proxy = buildSyntheticInterfaceProxy();
+
+        assertTrue(proxy.getClass().isSynthetic(), "Proxy class should be 
synthetic");
+        assertFalse(proxy.getClass().isHidden(), "Proxy class should not be 
hidden");
+
+        BeanInfo beanInfo = new BeanInfo(context, proxy.getClass());
+
+        // Use reflection to access the internal operations map and verify
+        // that only ONE operation is registered for "echo", not two.
+        java.lang.reflect.Field opsField = 
BeanInfo.class.getDeclaredField("operations");
+        opsField.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        Map<String, List<MethodInfo>> operations = (Map<String, 
List<MethodInfo>>) opsField.get(beanInfo);
+
+        List<MethodInfo> echoOps = operations.get("echo");
+        assertNotNull(echoOps, "Should have operations for 'echo'");
+        assertEquals(1, echoOps.size(),
+                "Should have exactly 1 operation for 'echo' (not 2). "
+                + "Two entries means findMostSpecificOverride failed to detect 
the proxy's "
+                + "method as an override of the interface method.");
+    }
+
+    /**
+     * Verifies that lambda (hidden synthetic) classes still work correctly —
+     * the @Handler annotation on the interface should be discoverable.
+     */
+    @Test
+    void testLambdaFunctionalInterfacePreservesAnnotation() {
+        MyHandler handler = () -> "result";
+
+        assertTrue(handler.getClass().isSynthetic(), "Lambda class should be 
synthetic");
+        assertTrue(handler.getClass().isHidden(), "Lambda class should be 
hidden");
+
+        BeanInfo beanInfo = new BeanInfo(context, handler.getClass());
+        assertTrue(beanInfo.hasAnyMethodHandlerAnnotation(),
+                "Handler annotation on interface should be discoverable 
through lambda");
+    }
+
+    /**
+     * Verifies that a synthetic subclass proxy (like CGLIB/ByteBuddy subclass 
proxies)
+     * still preserves annotation discovery on the parent class.
+     */
+    @Test
+    void testSyntheticSubclassProxyPreservesAnnotation() throws Exception {
+        Object proxy = new ByteBuddy()
+                .subclass(MyHandlerBean.class)
+                .modifiers(SyntheticState.SYNTHETIC, Visibility.PUBLIC)
+                .method(ElementMatchers.named("handle"))
+                .intercept(InvocationHandlerAdapter.of((p, method, args) -> 
"proxied"))
+                .make()
+                .load(getClass().getClassLoader())
+                .getLoaded()
+                .getDeclaredConstructor()
+                .newInstance();
+
+        assertTrue(proxy.getClass().isSynthetic());
+
+        BeanInfo beanInfo = new BeanInfo(context, proxy.getClass());
+        assertTrue(beanInfo.hasAnyMethodHandlerAnnotation(),
+                "Handler annotation on parent class should be discoverable 
through subclass proxy");
+    }
+
+    private Object buildSyntheticInterfaceProxy() throws Exception {
+        return new ByteBuddy()
+                .subclass(Object.class)
+                .implement(MyService.class)
+                .modifiers(SyntheticState.SYNTHETIC, Visibility.PUBLIC)
+                .method(ElementMatchers.named("echo"))
+                .intercept(InvocationHandlerAdapter.of((p, method, args) -> 
"Hello " + args[0]))
+                .make()
+                .load(getClass().getClassLoader())
+                .getLoaded()
+                .getDeclaredConstructor()
+                .newInstance();
+    }
+
+    // --- Test support types ---
+
+    public interface MyService {
+        String echo(String input);
+    }
+
+    @FunctionalInterface
+    public interface MyHandler {
+        @Handler
+        String handle();
+    }
+
+    public static class MyHandlerBean {
+        @Handler
+        public String handle() {
+            return "original";
+        }
+    }
+}

Reply via email to