Common classes used across Spring AI providing document processing, text transformation, embedding utilities, observability support, and tokenization capabilities for AI application development
Utility classes provide helper functions for JSON processing, string parsing, resource handling, logging with data classification, and template rendering.
The utilities layer consists of:
Jackson utility methods for module instantiation.
package org.springframework.ai.util;
import com.fasterxml.jackson.databind.Module;
import java.util.List;
abstract class JacksonUtils {
/**
* Instantiate well-known Jackson modules.
* Attempts to instantiate:
* - Jdk8Module (Java 8 types like Optional)
* - JavaTimeModule (Java 8 time types)
* - ParameterNamesModule (parameter name discovery)
* - KotlinModule (Kotlin support)
*
* Only modules available on classpath are instantiated.
* @return list of instantiated modules (empty list if no modules found)
*/
static List<Module> instantiateAvailableModules();
}import org.springframework.ai.util.JacksonUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.Module;
import java.util.List;
// Get available Jackson modules
List<Module> modules = JacksonUtils.instantiateAvailableModules();
System.out.println("Found " + modules.size() + " Jackson modules");
// Register modules with ObjectMapper
ObjectMapper mapper = new ObjectMapper();
modules.forEach(mapper::registerModule);
// Now mapper supports Java 8 types, time types, etc.
// Example usage
class Data {
private Optional<String> optionalField;
private LocalDateTime timestamp;
private List<Integer> numbers;
// getters/setters
}
Data data = new Data();
data.setOptionalField(Optional.of("value"));
data.setTimestamp(LocalDateTime.now());
data.setNumbers(List.of(1, 2, 3));
String json = mapper.writeValueAsString(data);
System.out.println(json);
Data deserialized = mapper.readValue(json, Data.class);import org.springframework.ai.util.JacksonUtils;
import com.fasterxml.jackson.databind.Module;
import java.util.List;
// Discover and log available modules
List<Module> modules = JacksonUtils.instantiateAvailableModules();
for (Module module : modules) {
System.out.println("Module: " + module.getModuleName());
System.out.println("Version: " + module.version());
}
// Typical output (if all dependencies are present):
// Module: jackson-module-parameter-names
// Module: jackson-datatype-jdk8
// Module: jackson-datatype-jsr310
// Module: jackson-module-kotlin (if Kotlin is on classpath)String parsing utilities for camel case handling.
package org.springframework.ai.util;
import java.util.List;
abstract class ParsingUtils {
/**
* Split camel case string into words.
* Example: "myVariableName" -> ["my", "Variable", "Name"]
* @param source source string in camel case
* @return list of words
*/
static List<String> splitCamelCase(String source);
/**
* Split camel case string into lowercase words.
* Example: "myVariableName" -> ["my", "variable", "name"]
* @param source source string in camel case
* @return list of lowercase words
*/
static List<String> splitCamelCaseToLower(String source);
/**
* Re-concatenate camel case with delimiter.
* Example: "myVariableName", "-" -> "my-variable-name"
* @param source source string in camel case
* @param delimiter delimiter to use
* @return concatenated string
*/
static String reConcatenateCamelCase(String source, String delimiter);
}import org.springframework.ai.util.ParsingUtils;
import java.util.List;
// Split camel case
String camelCase = "myVariableName";
List<String> words = ParsingUtils.splitCamelCase(camelCase);
System.out.println(words); // [my, Variable, Name]
// Split to lowercase
List<String> lowerWords = ParsingUtils.splitCamelCaseToLower(camelCase);
System.out.println(lowerWords); // [my, variable, name]
// Re-concatenate with delimiter
String kebabCase = ParsingUtils.reConcatenateCamelCase(camelCase, "-");
System.out.println(kebabCase); // my-variable-name
String snakeCase = ParsingUtils.reConcatenateCamelCase(camelCase, "_");
System.out.println(snakeCase); // my_variable_name
String spaced = ParsingUtils.reConcatenateCamelCase(camelCase, " ");
System.out.println(spaced); // my variable name
// More examples
String className = "DocumentTransformer";
System.out.println(ParsingUtils.reConcatenateCamelCase(className, "-"));
// document-transformer
String methodName = "getFormattedContent";
System.out.println(ParsingUtils.reConcatenateCamelCase(methodName, "_"));
// get_formatted_contentimport org.springframework.ai.util.ParsingUtils;
/**
* Convert Java naming conventions to different formats.
*/
class NamingConverter {
/**
* Convert Java class name to URL path.
*/
public static String toUrlPath(String className) {
return "/" + ParsingUtils.reConcatenateCamelCase(className, "-").toLowerCase();
}
/**
* Convert method name to property key.
*/
public static String toPropertyKey(String methodName) {
// Remove "get" or "set" prefix
String name = methodName;
if (name.startsWith("get") || name.startsWith("set")) {
name = name.substring(3);
}
return ParsingUtils.reConcatenateCamelCase(name, ".").toLowerCase();
}
/**
* Convert to human-readable label.
*/
public static String toLabel(String identifier) {
List<String> words = ParsingUtils.splitCamelCaseToLower(identifier);
return words.stream()
.map(word -> word.substring(0, 1).toUpperCase() + word.substring(1))
.collect(java.util.stream.Collectors.joining(" "));
}
}
// Usage
System.out.println(NamingConverter.toUrlPath("UserProfile"));
// /user-profile
System.out.println(NamingConverter.toPropertyKey("getUserName"));
// user.name
System.out.println(NamingConverter.toLabel("firstName"));
// First NameResource utility methods for loading content.
package org.springframework.ai.util;
abstract class ResourceUtils {
/**
* Get resource content as UTF-8 string.
* Supports classpath:, file:, and http(s): URIs.
* @param uri resource URI
* @return resource content as string
*/
static String getText(String uri);
}import org.springframework.ai.util.ResourceUtils;
// Load from classpath
String classpathContent = ResourceUtils.getText("classpath:data/template.txt");
System.out.println("Classpath content: " + classpathContent);
// Load from file system
String fileContent = ResourceUtils.getText("file:/path/to/document.txt");
System.out.println("File content: " + fileContent);
// Load from URL
String urlContent = ResourceUtils.getText("https://example.com/data.txt");
System.out.println("URL content: " + urlContent);
// Use in prompt templates
String promptTemplate = ResourceUtils.getText("classpath:prompts/system-prompt.txt");
String userQuery = "What is Spring AI?";
String fullPrompt = promptTemplate.replace("{query}", userQuery);
// Load configuration
String configJson = ResourceUtils.getText("classpath:config/ai-config.json");
// Parse JSON configurationimport org.springframework.ai.util.ResourceUtils;
import java.util.Map;
/**
* Load and process prompt templates from resources.
*/
class PromptTemplateLoader {
/**
* Load template and replace variables.
*/
public static String loadAndFormat(String templateUri, Map<String, String> variables) {
String template = ResourceUtils.getText(templateUri);
String result = template;
for (Map.Entry<String, String> entry : variables.entrySet()) {
String placeholder = "{" + entry.getKey() + "}";
result = result.replace(placeholder, entry.getValue());
}
return result;
}
}
// Usage
Map<String, String> vars = Map.of(
"name", "Spring AI",
"version", "1.1.2",
"language", "Java"
);
String prompt = PromptTemplateLoader.loadAndFormat(
"classpath:prompts/introduction.txt",
vars
);
System.out.println(prompt);Predefined SLF4J markers for data classification in logs.
package org.springframework.ai.util;
import org.slf4j.Marker;
abstract class LoggingMarkers {
/**
* Marker for sensitive business data.
* Use for data that should be protected within organization.
*/
static final Marker SENSITIVE_DATA_MARKER;
/**
* Marker for restricted data (credentials, secrets, IP).
* Use for data that should never be logged in production.
*/
static final Marker RESTRICTED_DATA_MARKER;
/**
* Marker for regulated data (PCI, PHI, PII).
* Use for data subject to compliance regulations.
*/
static final Marker REGULATED_DATA_MARKER;
/**
* Marker for public data.
* Use for data safe to log in all environments.
*/
static final Marker PUBLIC_DATA_MARKER;
}import org.springframework.ai.util.LoggingMarkers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class SecureLoggingExample {
private static final Logger logger = LoggerFactory.getLogger(SecureLoggingExample.class);
public void processUserData(String userId, String email, String apiKey,
String publicInfo) {
// Public data - safe to log
logger.info(LoggingMarkers.PUBLIC_DATA_MARKER,
"Processing request for user ID: {}", userId);
// Sensitive business data - log carefully
logger.debug(LoggingMarkers.SENSITIVE_DATA_MARKER,
"User email: {}", email);
// Restricted data - should never be logged in production
logger.trace(LoggingMarkers.RESTRICTED_DATA_MARKER,
"API key validation (first 4 chars): {}...", apiKey.substring(0, 4));
// Regulated data - subject to compliance
logger.debug(LoggingMarkers.REGULATED_DATA_MARKER,
"Processing PII for user: {}", userId);
// Public information
logger.info(LoggingMarkers.PUBLIC_DATA_MARKER,
"Public info: {}", publicInfo);
}
}Configure Logback to filter based on markers:
<!-- logback.xml -->
<configuration>
<!-- Console appender - exclude sensitive data -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<matcher>
<Name>RESTRICTED_FILTER</Name>
<Value>RESTRICTED_DATA</Value>
</matcher>
<expression>marker != null && marker.contains("RESTRICTED_DATA")</expression>
</evaluator>
<OnMatch>DENY</OnMatch>
<OnMismatch>NEUTRAL</OnMismatch>
</filter>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Secure file appender - include all data -->
<appender name="SECURE_FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/secure.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%marker] - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="SECURE_FILE" />
</root>
</configuration>import org.springframework.ai.util.LoggingMarkers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implement security-conscious logging throughout application.
*/
class SecurityAwareLogger {
private final Logger logger;
private final boolean productionMode;
public SecurityAwareLogger(Class<?> clazz, boolean productionMode) {
this.logger = LoggerFactory.getLogger(clazz);
this.productionMode = productionMode;
}
/**
* Log public information (always safe).
*/
public void logPublic(String message, Object... args) {
logger.info(LoggingMarkers.PUBLIC_DATA_MARKER, message, args);
}
/**
* Log sensitive data (only in non-production).
*/
public void logSensitive(String message, Object... args) {
if (!productionMode) {
logger.debug(LoggingMarkers.SENSITIVE_DATA_MARKER, message, args);
}
}
/**
* Log restricted data (only in development with explicit flag).
*/
public void logRestricted(String message, Object... args) {
if (!productionMode && logger.isTraceEnabled()) {
logger.trace(LoggingMarkers.RESTRICTED_DATA_MARKER, message, args);
}
}
/**
* Log regulated data (with compliance awareness).
*/
public void logRegulated(String message, Object... args) {
// Only log if compliance logging is enabled
if (isComplianceLoggingEnabled()) {
logger.debug(LoggingMarkers.REGULATED_DATA_MARKER, message, args);
}
}
private boolean isComplianceLoggingEnabled() {
// Check if compliance logging is enabled
return false; // Placeholder
}
}
// Usage
SecurityAwareLogger secureLogger = new SecurityAwareLogger(
MyService.class,
isProduction()
);
secureLogger.logPublic("Request processed successfully");
secureLogger.logSensitive("User email: {}", userEmail);
secureLogger.logRestricted("API key: {}", apiKey);
secureLogger.logRegulated("PII data: {}", personalInfo);Template rendering interface for dynamic content generation.
package org.springframework.ai.template;
import java.util.Map;
import java.util.function.BiFunction;
interface TemplateRenderer extends BiFunction<String, Map<String, Object>, String> {
/**
* Render template with variables.
* @param template template string
* @param variables map of variable names to values
* @return rendered string
*/
String apply(String template, Map<String, Object> variables);
}package org.springframework.ai.template;
import java.util.Map;
class NoOpTemplateRenderer implements TemplateRenderer {
/**
* Create no-op renderer.
*/
NoOpTemplateRenderer();
/**
* Return template unchanged.
* Variables are ignored.
* @param template template string (must not be null or empty)
* @param variables variables (ignored)
* @return template unchanged
* @throws IllegalArgumentException if template is null or empty
*/
String apply(String template, Map<String, Object> variables);
}package org.springframework.ai.template;
enum ValidationMode {
/**
* Throw exception on validation errors (default).
*/
THROW,
/**
* Log warning on validation errors.
*/
WARN,
/**
* Skip validation.
*/
NONE;
}import org.springframework.ai.template.TemplateRenderer;
import org.springframework.ai.template.NoOpTemplateRenderer;
import java.util.Map;
// No-op renderer (pass-through)
TemplateRenderer noOpRenderer = new NoOpTemplateRenderer();
String template = "Hello, {name}! Welcome to {product}.";
Map<String, Object> variables = Map.of(
"name", "Alice",
"product", "Spring AI"
);
String result = noOpRenderer.apply(template, variables);
System.out.println(result);
// Output: "Hello, {name}! Welcome to {product}." (unchanged)
// Custom renderer implementation
TemplateRenderer simpleRenderer = (template, vars) -> {
String result = template;
for (Map.Entry<String, Object> entry : vars.entrySet()) {
String placeholder = "{" + entry.getKey() + "}";
result = result.replace(placeholder, entry.getValue().toString());
}
return result;
};
String rendered = simpleRenderer.apply(template, variables);
System.out.println(rendered);
// Output: "Hello, Alice! Welcome to Spring AI."import org.springframework.ai.template.TemplateRenderer;
import org.springframework.ai.template.ValidationMode;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Custom template renderer with validation.
*/
class CustomTemplateRenderer implements TemplateRenderer {
private final ValidationMode validationMode;
private final Pattern placeholderPattern = Pattern.compile("\\{([^}]+)\\}");
public CustomTemplateRenderer(ValidationMode validationMode) {
this.validationMode = validationMode;
}
@Override
public String apply(String template, Map<String, Object> variables) {
// Validate if needed
if (validationMode == ValidationMode.THROW) {
validateTemplate(template, variables);
}
// Render template
StringBuffer result = new StringBuffer();
Matcher matcher = placeholderPattern.matcher(template);
while (matcher.find()) {
String key = matcher.group(1);
Object value = variables.get(key);
if (value != null) {
matcher.appendReplacement(result, value.toString());
} else if (validationMode == ValidationMode.WARN) {
System.err.println("Warning: Missing variable: " + key);
matcher.appendReplacement(result, "{" + key + "}");
} else {
matcher.appendReplacement(result, "{" + key + "}");
}
}
matcher.appendTail(result);
return result.toString();
}
private void validateTemplate(String template, Map<String, Object> variables) {
Matcher matcher = placeholderPattern.matcher(template);
while (matcher.find()) {
String key = matcher.group(1);
if (!variables.containsKey(key)) {
throw new IllegalArgumentException(
"Missing required variable: " + key
);
}
}
}
}
// Usage
TemplateRenderer strictRenderer = new CustomTemplateRenderer(ValidationMode.THROW);
TemplateRenderer lenientRenderer = new CustomTemplateRenderer(ValidationMode.WARN);
String template = "Hello, {name}! You have {count} messages.";
try {
// This will throw exception (missing "count")
strictRenderer.apply(template, Map.of("name", "Alice"));
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
}
// This will warn but continue
String result = lenientRenderer.apply(template, Map.of("name", "Alice"));
System.out.println(result);
// Output: "Hello, Alice! You have {count} messages."
// Warning: Missing variable: countimport org.springframework.ai.util.ResourceUtils;
import org.springframework.ai.util.ParsingUtils;
import org.springframework.ai.util.LoggingMarkers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
/**
* Example combining multiple utilities.
*/
class IntegratedUtilities {
private static final Logger logger = LoggerFactory.getLogger(IntegratedUtilities.class);
public String loadAndProcessTemplate(String resourceUri, Map<String, String> variables) {
// Load template using ResourceUtils
logger.info(LoggingMarkers.PUBLIC_DATA_MARKER,
"Loading template from: {}", resourceUri);
String template = ResourceUtils.getText(resourceUri);
// Process template
String result = template;
for (Map.Entry<String, String> entry : variables.entrySet()) {
// Convert camelCase keys to snake_case placeholders
String placeholder = "{" +
ParsingUtils.reConcatenateCamelCase(entry.getKey(), "_") + "}";
result = result.replace(placeholder, entry.getValue());
}
logger.debug(LoggingMarkers.SENSITIVE_DATA_MARKER,
"Template processed with {} variables", variables.size());
return result;
}
}
// Usage
IntegratedUtilities utils = new IntegratedUtilities();
Map<String, String> vars = Map.of(
"userName", "Alice",
"documentCount", "42"
);
String result = utils.loadAndProcessTemplate(
"classpath:templates/welcome.txt",
vars
);
// Template placeholders: {user_name}, {document_count}Thread Safety:
JacksonUtils: Thread-safe (static utility methods)ParsingUtils: Thread-safe (static utility methods)ResourceUtils: Thread-safe for reading (stateless)LoggingMarkers: Thread-safe (static final markers)TemplateRenderer: Depends on implementation (NoOpTemplateRenderer is thread-safe)Performance:
JacksonUtils.instantiateAvailableModules(): Expensive, call once at startupParsingUtils: O(n) where n is string lengthResourceUtils.getText(): I/O bound, depends on resource locationCommon Exceptions:
IOException: Resource not found, network errors (ResourceUtils)IllegalArgumentException: Invalid parameters (null/empty strings)ClassNotFoundException: Module not on classpath (JacksonUtils, silently skipped)RuntimeException: Unexpected errors (encoding issues, resource access failures)Edge Cases:
// JacksonUtils - missing modules
List<Module> modules = JacksonUtils.instantiateAvailableModules();
// Returns empty list if no modules found (not an error)
// ParsingUtils - edge cases
ParsingUtils.splitCamelCase("lowercase"); // Returns ["lowercase"]
ParsingUtils.splitCamelCase("UPPERCASE"); // Returns ["UPPERCASE"]
ParsingUtils.splitCamelCase(""); // Returns empty list
ParsingUtils.reConcatenateCamelCase("", "-"); // Returns ""
// ResourceUtils - missing resource
try {
String text = ResourceUtils.getText("classpath:missing.txt");
// Throws IOException wrapped in RuntimeException
} catch (RuntimeException e) {
// Handle missing resource
}
// ResourceUtils - network timeout
try {
String text = ResourceUtils.getText("https://slow-server.com/data.txt");
// May timeout or throw IOException
} catch (RuntimeException e) {
// Handle network error
}
// LoggingMarkers - null safety
Logger logger = // ...
logger.info(null, "message"); // Marker can be null (no marker applied)
// TemplateRenderer - validation
NoOpTemplateRenderer renderer = new NoOpTemplateRenderer();
try {
renderer.apply(null, variables); // Throws IllegalArgumentException
} catch (IllegalArgumentException e) {
// Handle null template
}
try {
renderer.apply("", variables); // Throws IllegalArgumentException
} catch (IllegalArgumentException e) {
// Handle empty template
}instantiateAvailableModules() once at startupLogging Security:
// NEVER log credentials without protection
logger.info("API Key: " + apiKey); // BAD
// Use RESTRICTED_DATA_MARKER and appropriate log filtering
logger.trace(LoggingMarkers.RESTRICTED_DATA_MARKER, "API Key: {}", apiKey); // GOOD
// Configure logback to filter RESTRICTED_DATA in productionResource Loading Security:
// Validate resource paths to prevent path traversal
String userInput = // ... from user
if (userInput.contains("..") || userInput.contains("~")) {
throw new SecurityException("Invalid resource path");
}
// Use classpath resources when possible (safer than file system)
String text = ResourceUtils.getText("classpath:safe/path.txt");Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-ai--spring-ai-commons@1.1.0