{"record":{"id":"dc64de46a3fdbeb7","repo":"flowable/flowable-engine","slug":"custom-properties-resolver-delegate-expression","errorCode":null,"errorMessage":"Custom properties resolver delegate expression \" + expression + \" did not resolve to an implementation of \" + CustomPropertiesResolver.class","messagePattern":"Custom properties resolver delegate expression \" \\+ expression \\+ \" did not resolve to an implementation of \" \\+ CustomPropertiesResolver\\.class","errorType":"exception","errorClass":"FlowableIllegalArgumentException","httpStatus":null,"severity":"error","filePath":"modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/listener/DelegateExpressionCustomPropertiesResolver.java","lineNumber":43,"sourceCode":"public class DelegateExpressionCustomPropertiesResolver implements CustomPropertiesResolver {\n\n    protected Expression expression;\n\n    public DelegateExpressionCustomPropertiesResolver(Expression expression) {\n        this.expression = expression;\n    }\n\n    @Override\n    public Map<String, Object> getCustomPropertiesMap(DelegateExecution execution) {\n        // Note: we can't cache the result of the expression, because the\n        // execution can change: eg.\n        // delegateExpression='${mySpringBeanFactory.randomSpringBean()}'\n        Object delegate = expression.getValue(execution);\n\n        if (delegate instanceof CustomPropertiesResolver) {\n            return ((CustomPropertiesResolver) delegate).getCustomPropertiesMap(execution);\n        } else {\n            throw new FlowableIllegalArgumentException(\"Custom properties resolver delegate expression \" + expression + \" did not resolve to an implementation of \" + CustomPropertiesResolver.class);\n        }\n    }\n\n    /**\n     * returns the expression text for this execution listener. Comes in handy if you want to check which listeners you already have.\n     */\n    public String getExpressionText() {\n        return expression.getExpressionText();\n    }\n\n}\n","sourceCodeStart":25,"sourceCodeEnd":55,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/listener/DelegateExpressionCustomPropertiesResolver.java#L25-L55","documentation":"Flowable's DelegateExpressionCustomPropertiesResolver evaluates a delegate expression (e.g. '${someBean}') at runtime and expects the resolved object to implement CustomPropertiesResolver. When the expression resolves to an object of another type, getCustomPropertiesMap throws FlowableIllegalArgumentException. This fail-fast check prevents an unsafe cast later in listener dispatch.","triggerScenarios":"A custom properties resolver is configured via delegate expression (flowable:customPropertiesResolverDelegateExpression or listener custom-properties resolver referencing an expression), the expression evaluates successfully (bean exists), but the returned bean does not implement org.flowable.engine.delegate.CustomPropertiesResolver.","commonSituations":"Spring bean wired to the wrong class or an interface misspelled in the bean's implements clause; refactoring renamed/moved CustomPropertiesResolver (e.g. org.flowable.engine.impl.bpmn.listener vs org.flowable.engine.delegate) so the bean implements the wrong package's interface; expression returning a proxy (Spring AOP) that fails instanceof; reusing an execution-listener bean as a custom-properties resolver.","solutions":["Make the target bean implement org.flowable.engine.delegate.CustomPropertiesResolver and its getCustomPropertiesMap(DelegateExecution) method.","Verify the expression actually points at the intended bean (e.g. '${myResolver}' resolving to a different bean of the same name in a different module).","Check for Spring AOP proxies: if @Transactional/@Async wraps the bean, ensure the proxy still implements the interface (interface-based proxying).","If the expression is a factory call like '${factory.create()}', confirm the factory returns a CustomPropertiesResolver instance, not a raw object."],"exampleFix":"// before\npublic class MyResolver {\n  public Map<String, Object> getProps(DelegateExecution ex) { ... }\n}\n// after\nimport org.flowable.engine.delegate.CustomPropertiesResolver;\npublic class MyResolver implements CustomPropertiesResolver {\n  @Override\n  public Map<String, Object> getCustomPropertiesMap(DelegateExecution execution) { ... }\n}","handlingStrategy":"type-guard","validationCode":"Object bean = applicationContext.getBean(\"myResolver\");\nif (!(bean instanceof org.flowable.engine.delegate.CustomPropertiesResolver)) {\n    throw new IllegalStateException(\"Bean 'myResolver' must implement CustomPropertiesResolver\");\n}","typeGuard":"public static boolean isCustomPropertiesResolver(Object o) {\n    return o instanceof org.flowable.engine.delegate.CustomPropertiesResolver;\n}","tryCatchPattern":"try {\n    listener.getCustomPropertiesMap(execution);\n} catch (org.flowable.common.engine.api.FlowableIllegalArgumentException e) {\n    log.error(\"Delegate expression did not resolve to a CustomPropertiesResolver: {}\", e.getMessage());\n    throw new ConfigurationException(\"Fix the delegateExpression to point at a CustomPropertiesResolver bean\", e);\n}","preventionTips":["Always implement org.flowable.engine.delegate.CustomPropertiesResolver explicitly and add a unit test asserting the bean's type.","Keep one canonical resolver interface import in the codebase and forbid legacy/wrong-package imports via checkstyle.","When using Spring AOP proxies, verify proxy.getTargetClass() still implements the expected interface.","Write an integration test that boots the process definition and evaluates every delegate expression before deploying."],"tags":["flowable","bpmn","delegate-expression","listener","type-mismatch"],"backgroundTag":"type-mismatch","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}