DEV Community

Parth B Thakkar
Parth B Thakkar

Posted on

Introducing Custom Annotations: The Power of @StringProcessor in Spring Boot

Welcome to "Apprentice to Architect: Parth’s Spring Boot Exploration"

Hi there! I’m excited to have you join me on this journey through Spring Boot. In "Apprentice to Architect," I’ll be sharing practical insights and techniques to help you master Spring Boot. Whether you're new to Spring Boot or looking to sharpen your skills, this series is designed to guide you from the basics to more advanced concepts.

What’s This Series All About?

This series is all about exploring the powerful features of Spring Boot and learning how to apply them effectively. Each chapter is crafted to build on the previous one, providing you with a step-by-step approach to mastering Spring Boot. We'll dive into custom annotations, AOP, and other advanced concepts, making your Spring Boot applications more robust and maintainable.

Chapter 1: Introducing Custom Annotations - The Power of @StringProcessor in Spring Boot

In this chapter, we’ll dive into the world of custom annotations with a focus on the @StringProcessor annotation. This powerful tool allows us to apply various text processing rules to string fields, making our code more expressive and easier to maintain. We’ll cover how to define this custom annotation and integrate it with other parts of your Spring Boot application.

What You’ll Discover

  • Defining Custom Annotations: Learn how to create and configure custom annotations to add metadata and behavior to your fields.
  • Applying Aspect-Oriented Programming (AOP): Discover how to use AOP to apply the custom annotation’s rules across your application, ensuring consistent text processing without cluttering your business logic.
  • Integrating with DTOs and Services: See how to use the @StringProcessor annotation with data transfer objects (DTOs) and services, enhancing your application's data handling and processing capabilities.
  • Practical Examples: Work through real-world examples to see how the @StringProcessor annotation simplifies text processing tasks, including transformations, trimming, and default value handling.

Each section is designed to provide you with hands-on experience and practical knowledge, helping you to apply these concepts effectively in your own projects. Let’s get started and unlock the power of custom annotations in Spring Boot!

Prerequisites

Before diving into the details, make sure you have a basic understanding of the following:

  • Java Basics: Familiarity with Java programming, including classes, methods, and basic syntax.
  • Spring Boot Fundamentals: Basic knowledge of Spring Boot, including creating a Spring Boot application and understanding common annotations.
  • Aspect-Oriented Programming (AOP): A general idea of AOP concepts and how they fit into Spring’s ecosystem.

Setting Up Your Project

To follow along with this chapter, you’ll need a Spring Boot project set up. Here’s a quick overview of what you should have:

  1. Spring Boot Application: A running Spring Boot application with basic setup.
  2. Maven/Gradle Dependencies: Ensure you have the necessary dependencies for Spring AOP and other relevant libraries.

You can use Spring Initializr to bootstrap your project if you haven’t already.

Step-by-Step Guide

Step 1: Creating the @StringProcessor Annotation

Define the custom annotation in a new Java file. Here’s what it should look like:

package com.parthbt143.springboot.innovations.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Annotation to specify text processing rules for String fields.
 * Applies transformations like uppercase, lowercase, capitalization of words or sentences, and other text processing options.
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface StringProcessor {

    /**
     * Enum to define the types of text transformations.
     */
    enum TransformType {
        NONE,                // No transformation applied
        UPPERCASE,           // Convert text to uppercase
        LOWERCASE,           // Convert text to lowercase
        CAPITALIZE_WORDS,   // Capitalize the first letter of every word
        CAPITALIZE_SENTENCES // Capitalize the first letter of every sentence
    }

    /**
     * Specifies the type of text transformation to apply.
     * Default is NONE (no transformation).
     *
     * @return the type of text transformation
     */
    TransformType transform() default TransformType.NONE;

    /**
     * Indicates whether to trim leading and trailing spaces.
     * Default is true.
     *
     * @return true if leading and trailing spaces should be trimmed, false otherwise
     */
    boolean trimSpaces() default true;

    /**
     * Indicates whether to replace multiple spaces between words with a single space.
     * Default is true.
     *
     * @return true if multiple spaces between words should be replaced, false otherwise
     */
    boolean trimMultipleSpaces() default true;

    /**
     * Specifies the maximum length for the string.
     * Default is -1 (no length constraint).
     *
     * @return the maximum length of the string
     */
    int maxLength() default -1;

    /**
     * Specifies the default value to use if the field is null.
     * Default is an empty string.
     *
     * @return the default value for the field
     */
    String defaultValue() default "";
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implementing the Aspect

Next, create an aspect to handle the processing of fields annotated with @StringProcessor. This aspect will intercept method calls and apply the annotation’s rules:

package com.parthbt143.springboot.innovations.aspects;

import com.parthbt143.springboot.innovations.annotations.StringProcessor;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;

/**
 * Aspect to process fields annotated with {@link StringProcessor}.
 * Applies transformations and processing rules as specified by the annotation.
 */
@Aspect
@Component
public class StringProcessorAspect {

    /**
     * Pointcut expression to match all methods in the specified package.
     */
    @Pointcut("execution(* com.parthbt143.springboot.innovations..*(..))")
    public void applicationLayer() {
    }

    /**
     * Advice that runs before the execution of methods matched by the pointcut.
     * Processes fields annotated with {@link StringProcessor} in method arguments.
     *
     * @param joinPoint the join point providing access to method arguments
     * @throws IllegalAccessException if access to the field is denied
     */
    @Before("applicationLayer()")
    public void applyStringProcessing(JoinPoint joinPoint) throws IllegalAccessException {
        for (Object arg : joinPoint.getArgs()) {
            processFields(arg);
        }
    }

    /**
     * Processes fields of an object that are annotated with {@link StringProcessor}.
     * Applies text transformations, trimming, and default values based on the annotation settings.
     *
     * @param obj the object to process
     * @throws IllegalAccessException if access to the field is denied
     */
    private void processFields(Object obj) throws IllegalAccessException {
        if (obj == null) return;

        for (Field field : obj.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(StringProcessor.class) && field.getType().equals(String.class)) {
                field.setAccessible(true);
                String value = (String) field.get(obj);

                StringProcessor processorAnnotation = field.getAnnotation(StringProcessor.class);

                // Apply default value if field is null or empty
                if (value == null || value.isEmpty()) {
                    value = processorAnnotation.defaultValue();
                }

                // Process value if not null
                if (value != null) {
                    value = processValue(value, processorAnnotation);
                    field.set(obj, value);
                }
            }
        }
    }

    /**
     * Processes the value of a string field according to the {@link StringProcessor} annotation settings.
     * Applies text transformation, enforces maximum length, trims spaces, and replaces multiple spaces.
     *
     * @param value               the value of the string field
     * @param processorAnnotation the annotation containing processing rules
     * @return the processed string value
     */
    private String processValue(String value, StringProcessor processorAnnotation) {
        // Apply text transformation based on annotation settings
        value = applyTextTransformation(value, processorAnnotation.transform());

        // Enforce maximum length if specified
        int maxLength = processorAnnotation.maxLength();
        if (maxLength > 0 && value.length() > maxLength) {
            value = value.substring(0, maxLength);
        }

        // Trim leading and trailing spaces if required
        if (processorAnnotation.trimSpaces()) {
            value = value.trim();
        }

        // Replace multiple spaces between words with a single space if required
        if (processorAnnotation.trimMultipleSpaces()) {
            value = value.replaceAll("\\s+", " ");
        }

        return value;
    }

    /**
     * Applies text transformation based on the specified type.
     *
     * @param value         the string value to transform
     * @param transformType the type of transformation to apply
     * @return the transformed string value
     */
    private String applyTextTransformation(String value, StringProcessor.TransformType transformType) {
        return switch (transformType) {
            case UPPERCASE -> value.toUpperCase();
            case LOWERCASE -> value.toLowerCase();
            case CAPITALIZE_WORDS -> capitalizeFirstLetters(value);
            case CAPITALIZE_SENTENCES -> capitalizeSentences(value);
            default -> value;
        };
    }

    /**
     * Capitalizes the first letter of each sentence in the input string.
     *
     * @param input the input string to process


     * @return the string with the first letter of each sentence capitalized
     */
    private String capitalizeSentences(String input) {
        String[] sentences = input.split("(?<=[.!?])\\s*");
        StringBuilder result = new StringBuilder();
        for (String sentence : sentences) {
            if (!sentence.isEmpty()) {
                result.append(Character.toUpperCase(sentence.charAt(0)))
                      .append(sentence.substring(1))
                      .append(" ");
            }
        }
        return result.toString().trim();
    }

    /**
     * Capitalizes the first letter of each word in the input string.
     *
     * @param input the input string to process
     * @return the string with the first letter of each word capitalized
     */
    private String capitalizeFirstLetters(String input) {
        String[] words = input.split("\\s+");
        StringBuilder result = new StringBuilder();
        for (String word : words) {
            if (!word.isEmpty()) {
                result.append(Character.toUpperCase(word.charAt(0)))
                      .append(word.substring(1))
                      .append(" ");
            }
        }
        return result.toString().trim();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Applying @StringProcessor to DTOs

Now, let’s use the annotation in a DTO to see it in action:

package com.parthbt143.springboot.innovations.DTOs;

import com.parthbt143.springboot.innovations.annotations.StringProcessor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {

    @StringProcessor(
            transform = StringProcessor.TransformType.CAPITALIZE_WORDS,
            defaultValue = "Unknown User"
    )
    private String fullName;

    @StringProcessor(
            transform = StringProcessor.TransformType.UPPERCASE
    )
    private String uniqueId;

    @StringProcessor(
            transform = StringProcessor.TransformType.LOWERCASE,
            defaultValue = "noemail@example.com"
    )
    private String emailAddress;


    @StringProcessor(
            transform = StringProcessor.TransformType.CAPITALIZE_SENTENCES,
            defaultValue = "Unknown Address"
    )
    private String address;

    @StringProcessor(
            maxLength = 250,
            trimMultipleSpaces = false,
            defaultValue = "N/A"
    )
    private String userDetails;

    @Override
    public String toString() {
        return String.format(
                "UserDTO [fullName=%s, uniqueId=%s, emailAddress=%s, address=%s, userDetails=%s]",
                fullName != null ? fullName : "null",
                uniqueId != null ? uniqueId : "null",
                emailAddress != null ? emailAddress : "null",
                address != null ? address : "null",
                userDetails != null ? userDetails : "null"
        );
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 4: Testing Your Implementation

Create a controller
To see the @StringProcessor in action, you can create a controller and service that process the UserDTO objects:

package com.parthbt143.springboot.innovations.controllers;

import com.parthbt143.springboot.innovations.DTOs.UserDTO;
import com.parthbt143.springboot.innovations.services.StringProcessorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class StringProcessorController {

    @Autowired
    private StringProcessorService stringProcessorService;

    @PostMapping("/get-processed-dto")
    public UserDTO process(@RequestBody UserDTO userDTO) {
        return stringProcessorService.processedUserDTO(userDTO);
    }
}
Enter fullscreen mode Exit fullscreen mode

Create a Service

package com.parthbt143.springboot.innovations.services;

import com.parthbt143.springboot.innovations.DTOs.UserDTO;
import org.springframework.stereotype.Service;

@Service
public class StringProcessorService {

    public UserDTO processedUserDTO(UserDTO userDTO) {
        System.out.println(userDTO.toString());
        return userDTO;
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this chapter, we explored how to create and use custom annotations in Spring Boot, with a focus on the @StringProcessor annotation. We covered how to define the annotation, implement the associated aspect, and apply it to your DTOs. By following along, you’ve learned how to make your Spring Boot applications more flexible and maintainable through custom annotations and AOP.

I hope you found this chapter insightful. Stay tuned for more as we continue to explore Spring Boot’s powerful features in the upcoming chapters!

Source Code

You can find the source code for this chapter in the GitHub repository.

Top comments (2)

Collapse
 
frankconnolly profile image
Frank Connolly • Edited

This is great! Is it possible for the UserDTO class to be a Java record and still use the custom annotations?

Collapse
 
parthbt143 profile image
Parth B Thakkar

Yes, You'll be able to use this annotation with record too.