Expression Language
Resolve annotation attribute values dynamically using Jakarta EL and MicroProfile Config expressions.
Expression Language
LangChain4j CDI supports dynamic resolution of annotation attribute values through an ExpressionResolver SPI. Two built-in implementations are provided:
| Syntax | Module | Resolver |
|---|---|---|
#{...} |
langchain4j-cdi-el |
Jakarta Expression Language |
${...} |
langchain4j-cdi-config |
MicroProfile Config |
Both can coexist -- their delimiters are distinct, so there is no conflict.
Where Expressions Are Supported
Expressions can be used in any annotation attribute that resolves a bean name or metadata value:
- AI Service attributes --
chatModelName,streamingChatModelName,toolProviderName,chatMemoryName,chatMemoryProviderName,contentRetrieverName,retrievalAugmentorName - Agent attributes --
name,description,outputKey,subAgentNames,chatModelName,agentListenerName,errorHandlerName,outputProviderName,beforeCallName, and all other*Nameattributes - Agent topology-specific attributes --
supervisorPromptName,exitConditionName,a2aServerUrl,mcpClientName,mcpToolNames,plannerName, etc.
Jakarta EL Expressions
Setup
Add the langchain4j-cdi-el module:
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-el</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>
In managed environments (WildFly, GlassFish, Liberty, Payara, Quarkus) a Jakarta EL runtime is already provided. In standalone or test scenarios, add an implementation explicitly:
<dependency>
<groupId>org.glassfish.expressly</groupId>
<artifactId>expressly</artifactId>
<version>5.0.0</version>
<scope>runtime</scope>
</dependency>
How It Works
When langchain4j-cdi-el is on the classpath, the JakartaELExpressionResolver is discovered automatically via java.util.ServiceLoader. Any annotation attribute value matching #{...} (the expression must occupy the entire value) is evaluated as a Jakarta EL expression.
CDI integration: When CDI is active, @Named beans are directly accessible. For example, #{myConfig.modelName} calls getModelName() on the @Named("myConfig") bean.
Non-CDI fallback: When CDI is not active (e.g. during unit tests), basic EL evaluation still works: arithmetic, string operations, conditionals, and standard implicit objects like systemProperties.
Failure handling: If evaluation fails, the original #{...} value is returned unchanged and a WARNING is logged.
Examples
Conditional bean selection:
@RegisterAIService(chatModelName = "#{config.useGpu ? 'gpu-model' : 'cpu-model'}")
public interface SmartAssistant {
String chat(String message);
}
CDI bean property lookup:
@Named("agentConfig")
@ApplicationScoped
public class AgentConfig {
public String getReviewerName() {
return "reviewer-agent";
}
}
@RegisterSimpleAgent(
name = "#{agentConfig.reviewerName}",
chatModelName = "#default"
)
public interface ReviewAgent {}
System properties:
@RegisterAIService(chatModelName = "#{systemProperties['chat.model.bean']}")
public interface EnvironmentAwareService {
String chat(String message);
}
String operations and arithmetic:
@RegisterSimpleAgent(
name = "#{'agent-' += systemProperties['app.env']}",
chatModelName = "#default"
)
public interface EnvAgent {}
MicroProfile Config Expressions
Setup
Add the langchain4j-cdi-config module:
<dependency>
<groupId>dev.langchain4j.cdi.mp</groupId>
<artifactId>langchain4j-cdi-config</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>
How It Works
The MpConfigExpressionResolver matches ${...} values and resolves the enclosed key via ConfigProvider.getConfig().getOptionalValue(key, String.class). If the key is not found, the original value is returned unchanged.
Examples
Externalize the model name:
# microprofile-config.properties
ai.chat-model.name=my-openai-model
@RegisterAIService(chatModelName = "${ai.chat-model.name}")
public interface ConfigDrivenService {
String chat(String message);
}
Configure agent sub-agents:
pipeline.step1=step1-agent
@RegisterSequenceAgent(
name = "pipeline",
subAgentNames = {"${pipeline.step1}", "step2", "step3"}
)
public interface PipelineAgent {}
Combining Both Expression Types
Both resolvers can coexist in the same application. All registered ExpressionResolver implementations are applied as a pipeline: the output of each feeds into the next. Since ${...} and #{...} use distinct delimiters, they never interfere with each other.
// Resolved via MicroProfile Config
@RegisterAIService(chatModelName = "${my.chat.model.name}")
public interface ConfigService {
String chat(String message);
}
// Resolved via Jakarta EL
@RegisterAIService(chatModelName = "#{runtimeConfig.chatModelName}")
public interface ElService {
String chat(String message);
}
Writing a Custom ExpressionResolver
The ExpressionResolver SPI is open for extension. Implement the interface and register it via ServiceLoader to support your own expression syntax.
Interface
package dev.langchain4j.cdi.spi;
public interface ExpressionResolver {
/**
* Resolves any expression embedded in value.
* Must return value unchanged if not recognised.
*/
String resolve(String value);
}
Example: Vault Secret Resolver
public class VaultExpressionResolver implements ExpressionResolver {
@Override
public String resolve(String value) {
if (!value.startsWith("vault:")) {
return value;
}
String secretPath = value.substring(6);
return VaultClient.readSecret(secretPath);
}
}
Register in META-INF/services/dev.langchain4j.cdi.spi.ExpressionResolver:
com.example.VaultExpressionResolver
Pipeline Ordering
ServiceLoader does not guarantee a fixed discovery order. The built-in resolvers use distinct, non-overlapping delimiters, so their relative order does not matter. Custom resolvers whose output could match another resolver's pattern should use the standard ServiceLoader ordering mechanisms (e.g. module layers or explicit ordering in module-info.java).
Summary of Name Resolution
All annotation attribute values go through this resolution chain before CDI bean lookup:
| Value | Resolution |
|---|---|
"#default" |
Default CDI bean of the expected type |
"" (empty) |
Ignored -- dependency not wired |
"myBean" |
CDI bean named myBean |
"${property.key}" |
MicroProfile Config property lookup |
"#{el.expression}" |
Jakarta EL evaluation |
"vault:secret/path" |
Custom resolver (if registered) |