Wrappers in Java and Spring Boot: What They Are, When to Use, and When to Create Custom Ones

Jefster Farlei Fernandes Caixeta Junior - Feb 17 - - Dev Community

Introduction

In Java development, especially in Spring Boot applications, wrappers play a crucial role in enhancing functionality, improving security, ensuring thread safety, and simplifying complex operations. While Java provides a robust collection framework and utility classes, there are cases where custom wrappers become essential.

This article explores what wrappers are, when to use them, and how to create custom ones tailored for real-world applications.

What Are Wrappers?

A wrapper is a design pattern that encapsulates an existing object or collection, adding additional functionality without modifying the original implementation. Wrappers are commonly used for:

  1. Read-only access (e.g., Collections.unmodifiableList)
  2. Thread safety (e.g., Collections.synchronizedList)
  3. Logging and debugging
  4. Caching and performance optimization
  5. Event-driven behavior (e.g., observable collections)

When to Use Wrappers

  • Wrappers are beneficial when:
  • Enhancing an existing collection or object without modifying its source code.
  • Implementing additional behaviors like logging, validation, or event notifications.
  • Ensuring security by restricting modifications to sensitive data.
  • Improving thread safety in concurrent applications.
  • Providing default values or error handling mechanisms.

Common Use Cases in Spring Boot Applications

Spring Boot applications often require custom wrappers to handle specific concerns. Some common scenarios include:

1. Thread-Safe Wrappers

Spring Boot applications often work in multi-threaded environments. Wrapping collections with synchronization mechanisms ensures safe concurrent access.

Example: A thread-safe Map using ReadWriteLock:

import java.util.*;
import java.util.concurrent.locks.*;

public class SynchronizedReadWriteMap<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(K key, V value) {
        lock.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }

    public V get(K key) {
        lock.readLock().lock();
        try {
            return map.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Logging and Auditing Wrappers

Logging wrappers help track modifications to collections, useful for debugging or auditing.

Example: A logging wrapper for List:

import java.util.*;

public class LoggingList<T> extends ArrayList<T> {
    @Override
    public boolean add(T t) {
        System.out.println("Adding element: " + t);
        return super.add(t);
    }

    @Override
    public boolean remove(Object o) {
        System.out.println("Removing element: " + o);
        return super.remove(o);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Read-Only Wrappers

To prevent modifications to a collection, an unmodifiable wrapper ensures data integrity.

Example:

import java.util.*;

public class UnmodifiableWrapper<T> extends ArrayList<T> {
    @Override
    public boolean add(T t) {
        throw new UnsupportedOperationException("Modification is not allowed");
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Observable Wrappers

Observable wrappers notify listeners when data changes, useful for reactive applications.

Example:

import java.util.*;

public class ObservableMap<K, V> extends HashMap<K, V> {
    private final List<Runnable> listeners = new ArrayList<>();

    public void addListener(Runnable listener) {
        listeners.add(listener);
    }

    private void notifyListeners() {
        for (Runnable listener : listeners) {
            listener.run();
        }
    }

    @Override
    public V put(K key, V value) {
        V result = super.put(key, value);
        notifyListeners();
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Auto-Expiring Cache Wrappers

For caching, a wrapper can remove entries after a certain time.

Example:

import java.util.*;
import java.util.concurrent.*;

public class ExpiringCache<K, V> {
    private final Map<K, V> cache = new ConcurrentHashMap<>();
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    public void put(K key, V value, long expirationTime, TimeUnit unit) {
        cache.put(key, value);
        scheduler.schedule(() -> cache.remove(key), expirationTime, unit);
    }

    public V get(K key) {
        return cache.get(key);
    }
}
Enter fullscreen mode Exit fullscreen mode

When to Create Custom Wrappers

While Java provides built-in wrappers likeCollections.synchronizedList and Collections.unmodifiableMap, creating custom wrappers is useful when:

You need business-specific behavior (e.g., access control on collections).

You require event-driven changes (e.g., auto-updating UI components in Spring Boot with WebSockets).

You must optimize performance (e.g., batching writes to a database).

You want to implement retry mechanisms (e.g., handling network failures in REST clients).

Conclusion

Wrappers are a powerful tool in Java and Spring Boot applications, enabling better modularity, security, and performance. Whether making collections thread-safe, adding observability, or implementing caching, custom wrappers can significantly improve your application's maintainability.

By understanding when and how to use them, you can enhance your software design and build more resilient applications. 🚀

.