Spring AOP module providing aspect-oriented programming capabilities for the Spring Framework
—
Different target source implementations for managing target object lifecycle, including singleton, prototype, pooled, thread-local, and hot-swappable target sources. Target sources provide flexible strategies for obtaining and managing the actual objects that AOP proxies delegate to, enabling advanced patterns like object pooling, lazy initialization, and runtime target replacement.
The fundamental interface for all target source implementations, defining how proxies obtain target objects.
public interface TargetSource extends TargetClassAware {
/**
* Return the type of targets returned by this {@link TargetSource}.
* <p>Can return {@code null}, although certain usages of a {@code TargetSource}
* might just work with a predetermined target class.
* @return the type of targets returned by this {@link TargetSource}
*/
@Override
Class<?> getTargetClass();
/**
* Will all calls to {@link #getTarget()} return the same object?
* <p>In that case, there will be no need to invoke {@link #releaseTarget(Object)},
* and the AOP framework can cache the return value of {@link #getTarget()}.
* @return {@code true} if the target is immutable
* @see #getTarget()
*/
boolean isStatic();
/**
* Return a target instance. Invoked immediately before the
* AOP framework calls the "target" of an AOP method invocation.
* @return the target object which contains the joinpoint,
* or {@code null} if there is no actual target instance
* @throws Exception if the target object can't be resolved
*/
Object getTarget() throws Exception;
/**
* Release the given target object obtained from the
* {@link #getTarget()} method, if any.
* @param target object obtained from a call to {@link #getTarget()}
* @throws Exception if the object can't be released
*/
void releaseTarget(Object target) throws Exception;
}Simple target source implementations for common scenarios.
public class SingletonTargetSource implements TargetSource, Serializable {
/**
* Create a new SingletonTargetSource for the given target.
* @param target the target object
*/
public SingletonTargetSource(Object target);
/**
* Return the target object.
*/
public final Object getTarget();
/**
* Set the target object for this TargetSource.
* @param target the target object
*/
public void setTarget(Object target);
@Override
public Class<?> getTargetClass();
@Override
public boolean isStatic();
@Override
public Object getTarget() throws Exception;
@Override
public void releaseTarget(Object target);
}
public class EmptyTargetSource implements TargetSource, Serializable {
/** The canonical (Singleton) instance of this {@link EmptyTargetSource}. */
public static final EmptyTargetSource INSTANCE = new EmptyTargetSource(null, true);
/**
* Create a new instance of the {@link EmptyTargetSource} class.
* <p>This constructor is {@code private} to enforce the
* Singleton pattern / Flyweight pattern.
* @param targetClass the target class
* @param isStatic whether the target source is static
*/
private EmptyTargetSource(Class<?> targetClass, boolean isStatic);
/**
* Return an EmptyTargetSource for the given target Class.
* @param targetClass the target Class (may be {@code null})
* @return the corresponding EmptyTargetSource instance
*/
public static EmptyTargetSource forClass(Class<?> targetClass);
/**
* Return an EmptyTargetSource for the given target Class.
* @param targetClass the target Class (may be {@code null})
* @param isStatic whether the target source should be flagged as static
* @return the corresponding EmptyTargetSource instance
*/
public static EmptyTargetSource forClass(Class<?> targetClass, boolean isStatic);
@Override
public Class<?> getTargetClass();
@Override
public boolean isStatic();
@Override
public Object getTarget();
@Override
public void releaseTarget(Object target);
}Target sources that obtain target objects from Spring BeanFactory instances.
public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSource, BeanFactoryAware, Serializable {
/** The owning BeanFactory. */
private BeanFactory beanFactory;
/** Name of the target bean we're proxying. */
private String targetBeanName;
/** Class of the target. */
private Class<?> targetClass;
/**
* Set the owning BeanFactory. We need to save a reference so that we can
* use the {@code getBean} method on every invocation.
*/
@Override
public final void setBeanFactory(BeanFactory beanFactory);
/**
* Return the owning BeanFactory.
*/
public final BeanFactory getBeanFactory();
/**
* Set the name of the target bean in the factory.
* <p>The target bean should not be a singleton, else the same instance will
* always be obtained from the factory, resulting in the same behavior as
* provided by {@link SingletonTargetSource}.
* @param targetBeanName name of the target bean in the BeanFactory
* that owns this interceptor
* @see SingletonTargetSource
*/
public void setTargetBeanName(String targetBeanName);
/**
* Return the name of the target bean in the factory.
*/
public String getTargetBeanName();
/**
* Set the target class explicitly, to avoid any kind of access to the
* target bean (for example, to avoid initialization of a FactoryBean instance).
* <p>Default is to detect the type automatically, through a {@code getType}
* call on the BeanFactory (or even a full {@code getBean} call as fallback).
*/
public void setTargetClass(Class<?> targetClass);
@Override
public Class<?> getTargetClass();
@Override
public boolean isStatic();
@Override
public void releaseTarget(Object target);
/**
* Copy configuration from the other AbstractBeanFactoryBasedTargetSource object.
* Subclasses should override this if they wish to expose it.
* @param other object to copy configuration from
*/
protected void copyFrom(AbstractBeanFactoryBasedTargetSource other);
}
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception;
}
public class LazyInitTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception;
}
public class PrototypeTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception;
/**
* Destroy the given independent instance.
* @param target the bean instance to destroy
* @see #getTarget()
*/
@Override
public void releaseTarget(Object target);
}Target source that allows runtime replacement of the target object.
public class HotSwappableTargetSource implements TargetSource, Serializable {
/** The current target object. */
private Object target;
/**
* Create a new HotSwappableTargetSource with the given initial target object.
* @param initialTarget the initial target object
*/
public HotSwappableTargetSource(Object initialTarget);
@Override
public Class<?> getTargetClass();
@Override
public final boolean isStatic();
@Override
public Object getTarget();
@Override
public void releaseTarget(Object target);
/**
* Swap the target, returning the old target object.
* @param newTarget the new target object
* @return the previous target object
* @throws IllegalArgumentException if the new target is invalid
*/
public synchronized Object swap(Object newTarget) throws IllegalArgumentException;
}Target source that maintains separate target instances per thread.
public class ThreadLocalTargetSource extends AbstractPrototypeBasedTargetSource
implements ThreadLocalTargetSourceStats, DisposableBean {
/**
* ThreadLocal holding the target associated with the current
* thread. Unlike most ThreadLocals, which are static, this variable
* is meant to be per-thread per-instance of the ThreadLocalTargetSource class.
*/
private final ThreadLocal<Object> targetInThread = new NamedThreadLocal<>("Thread-local instance of bean '" + getTargetBeanName() + "'");
@Override
public Object getTarget() throws BeansException;
/**
* Dispose of targets if necessary; clear ThreadLocal.
* @see #destroyPrototypeInstance
*/
@Override
public void releaseTarget(Object target);
@Override
public void destroy();
/**
* Return the number of client invocations.
*/
@Override
public int getInvocationCount();
/**
* Return the number of hits that were satisfied by a thread-bound object.
*/
@Override
public int getHitCount();
/**
* Return the number of thread-bound objects created.
*/
@Override
public int getObjectCount();
/**
* Reset the statistics maintained by this object.
*/
public void resetStatistics();
}
public interface ThreadLocalTargetSourceStats {
/**
* Return the number of client invocations.
*/
int getInvocationCount();
/**
* Return the number of hits that were satisfied by a thread-bound object.
*/
int getHitCount();
/**
* Return the number of thread-bound objects created.
*/
int getObjectCount();
}Target sources that implement object pooling strategies.
public interface PoolingConfig {
/**
* Return the maximum size of the pool.
*/
int getMaxSize();
/**
* Return the minimum size of the pool.
*/
int getMinSize();
/**
* Return the current number of active objects in the pool.
* @throws UnsupportedOperationException if not supported by the pool
*/
int getActiveCount() throws UnsupportedOperationException;
/**
* Return the current number of idle objects in the pool.
* @throws UnsupportedOperationException if not supported by the pool
*/
int getIdleCount() throws UnsupportedOperationException;
}
public abstract class AbstractPoolingTargetSource extends AbstractBeanFactoryBasedTargetSource implements PoolingConfig {
/** The maximum size of the pool. */
private int maxSize = -1;
/**
* Set the maximum size of the pool.
* Default is -1, indicating no size limit.
*/
public void setMaxSize(int maxSize);
@Override
public int getMaxSize();
/**
* Return the current number of active objects in the pool.
* @throws UnsupportedOperationException if not supported by the pool
*/
@Override
public final int getActiveCount() throws UnsupportedOperationException;
/**
* Return the current number of idle objects in the pool.
* @throws UnsupportedOperationException if not supported by the pool
*/
@Override
public final int getIdleCount() throws UnsupportedOperationException;
/**
* Subclasses must implement this to return the number of idle instances in the pool.
* @throws UnsupportedOperationException if not supported by the pool
*/
protected abstract int getIdleCountInternal() throws UnsupportedOperationException;
/**
* Subclasses must implement this to return the number of active instances in the pool.
* @throws UnsupportedOperationException if not supported by the pool
*/
protected abstract int getActiveCountInternal() throws UnsupportedOperationException;
}
public class CommonsPool2TargetSource extends AbstractPoolingTargetSource implements DisposableBean {
private ObjectPool<Object> pool;
private PooledObjectFactory<Object> pooledObjectFactory = new PooledObjectFactory<Object>() {
@Override
public PooledObject<Object> makeObject() throws Exception;
@Override
public void destroyObject(PooledObject<Object> p) throws Exception;
@Override
public boolean validateObject(PooledObject<Object> p);
@Override
public void activateObject(PooledObject<Object> p) throws Exception;
@Override
public void passivateObject(PooledObject<Object> p) throws Exception;
};
/**
* Create a CommonsPoolTargetSource with default settings.
* Default maximum size of the pool is 8.
* @see #setMaxSize
* @see GenericObjectPool#DEFAULT_MAX_TOTAL
*/
public CommonsPool2TargetSource();
/**
* Set the maximum number of idle objects in the pool.
* Default is 8.
* @see GenericObjectPool#setMaxIdle
*/
public void setMaxIdle(int maxIdle);
/**
* Return the maximum number of idle objects in the pool.
*/
public int getMaxIdle();
/**
* Set the minimum number of idle objects in the pool.
* Default is 0.
* @see GenericObjectPool#setMinIdle
*/
public void setMinIdle(int minIdle);
/**
* Return the minimum number of idle objects in the pool.
*/
@Override
public int getMinSize();
/**
* Set the maximum waiting time for the pool to return an object.
* Default is -1, meaning unlimited.
* @see GenericObjectPool#setMaxWaitMillis
*/
public void setMaxWait(long maxWait);
/**
* Return the maximum waiting time for the pool to return an object.
*/
public long getMaxWait();
/**
* Set the time between eviction runs that check for idle objects that can be removed.
* Default is -1, meaning no eviction thread will run.
* @see GenericObjectPool#setTimeBetweenEvictionRunsMillis
*/
public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis);
/**
* Return the time between eviction runs.
*/
public long getTimeBetweenEvictionRunsMillis();
/**
* Set the minimum time that an idle object can sit in the pool before
* it becomes subject to eviction. Default is 30 minutes.
* @see GenericObjectPool#setMinEvictableIdleTimeMillis
*/
public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis);
/**
* Return the minimum evictable idle time.
*/
public long getMinEvictableIdleTimeMillis();
/**
* Set whether objects created for the pool will be validated before being returned
* from the {@code borrowObject()} method. Validation is performed by the
* {@code validateObject()} method of the factory associated with the pool.
* Default is {@code false}.
* @see GenericObjectPool#setTestOnBorrow
*/
public void setTestOnBorrow(boolean testOnBorrow);
/**
* Return whether objects are validated before being returned from the pool.
*/
public boolean isTestOnBorrow();
/**
* Set whether objects created for the pool will be validated before being returned
* to the pool. Validation is performed by the {@code validateObject()} method of
* the factory associated with the pool. Default is {@code false}.
* @see GenericObjectPool#setTestOnReturn
*/
public void setTestOnReturn(boolean testOnReturn);
/**
* Return whether objects are validated before being returned to the pool.
*/
public boolean isTestOnReturn();
/**
* Set whether objects sitting idle in the pool will be validated by the idle object
* evictor (if any - see {@link #setTimeBetweenEvictionRunsMillis}). Validation is
* performed by the {@code validateObject()} method of the factory associated with the pool.
* Default is {@code false}.
* @see GenericObjectPool#setTestWhileIdle
*/
public void setTestWhileIdle(boolean testWhileIdle);
/**
* Return whether objects are validated by the idle object evictor.
*/
public boolean isTestWhileIdle();
/**
* Sets the config for this pool.
* @param config the new pool configuration to use
* @see GenericObjectPool#setConfig
*/
public void setConfig(GenericObjectPoolConfig<Object> config);
/**
* Creates and holds an ObjectPool instance.
* @see #createObjectPool()
*/
protected final void createPool();
/**
* Subclasses can override this if they want to return a specific Commons pool.
* They should apply any configuration properties to the pool here.
* <p>Default is a GenericObjectPool instance with the given pool size.
* @return an empty Commons {@code ObjectPool}.
* @see GenericObjectPool
* @see #setMaxSize
*/
protected ObjectPool<Object> createObjectPool();
@Override
public Object getTarget() throws Exception;
@Override
public void releaseTarget(Object target) throws Exception;
@Override
protected int getActiveCountInternal();
@Override
protected int getIdleCountInternal();
/**
* Closes the underlying {@code ObjectPool} when destroying this object.
*/
@Override
public void destroy() throws Exception;
}Target sources that can be refreshed or changed at runtime.
public interface Refreshable {
/**
* Refresh the underlying target object.
*/
void refresh();
/**
* Return the number of actual refreshes since startup.
*/
long getRefreshCount();
/**
* Return the timestamp of the last refresh attempt (successful or not).
*/
Date getLastRefreshTime();
}
public abstract class AbstractRefreshableTargetSource extends AbstractBeanFactoryBasedTargetSource implements Refreshable {
protected Object cachedTarget;
private long refreshCount;
private Date lastRefreshCheck;
private long lastRefreshTime = -1;
/**
* Cache a target if it is meant to be cached.
*/
@Override
public Object getTarget();
/**
* No need to release cached target.
*/
@Override
public void releaseTarget(Object object);
@Override
public final synchronized void refresh();
/**
* Determine a refresh timestamp, indicating the last time
* that a refresh attempt was made.
* <p>This implementation returns the current system time when
* the refresh attempt is being made.
* <p>Subclasses can override this to return an appropriate timestamp
* based on configuration settings, metadata analysis, or other factors.
* @return the refresh timestamp to expose through {@link #getLastRefreshTime()}
* @see #getLastRefreshTime()
*/
protected long determineLastRefreshTime();
@Override
public long getRefreshCount();
@Override
public Date getLastRefreshTime();
/**
* Determine whether a refresh is required.
* Invoked for each refresh check, after successful retrieval of a cached instance.
* <p>The default implementation always returns {@code true}, triggering
* a refresh every time the refresh check delay has elapsed.
* To be overridden by subclasses with an appropriate check of the
* underlying target resource.
* @param cachedTarget the cached target object
* @param refreshCheckDelay ms that have elapsed since the last refresh check
* @return whether a refresh is required
*/
protected boolean requiresRefresh(Object cachedTarget, long refreshCheckDelay);
/**
* Obtain a fresh target object.
* <p>Only invoked when a refresh check returned {@code true}.
* @return the fresh target object
*/
protected abstract Object freshTarget();
}
public class BeanFactoryRefreshableTargetSource extends AbstractRefreshableTargetSource {
@Override
protected final Object freshTarget();
}// Singleton target source (most common)
Object target = new MyServiceImpl();
SingletonTargetSource targetSource = new SingletonTargetSource(target);
ProxyFactory factory = new ProxyFactory();
factory.setTargetSource(targetSource);
factory.addInterface(MyService.class);
factory.addAdvice(new LoggingInterceptor());
MyService proxy = (MyService) factory.getProxy();
// Hot swappable target source
HotSwappableTargetSource swappableSource = new HotSwappableTargetSource(new MyServiceImpl());
ProxyFactory swappableFactory = new ProxyFactory();
swappableFactory.setTargetSource(swappableSource);
swappableFactory.addInterface(MyService.class);
MyService swappableProxy = (MyService) swappableFactory.getProxy();
// Runtime target replacement
Object oldTarget = swappableSource.swap(new EnhancedServiceImpl());
// All subsequent calls to swappableProxy will use the new target@Configuration
public class PrototypeTargetSourceConfig {
@Bean
@Scope("prototype")
public ExpensiveService expensiveService() {
return new ExpensiveService();
}
@Bean
public PrototypeTargetSource expensiveServiceTargetSource() {
PrototypeTargetSource targetSource = new PrototypeTargetSource();
targetSource.setTargetBeanName("expensiveService");
targetSource.setBeanFactory(applicationContext);
return targetSource;
}
@Bean
public ProxyFactoryBean expensiveServiceProxy() {
ProxyFactoryBean proxyFactory = new ProxyFactoryBean();
proxyFactory.setTargetSource(expensiveServiceTargetSource());
proxyFactory.setInterfaces(ExpensiveService.class);
return proxyFactory;
}
}// Configuration for thread-local target source
@Configuration
public class ThreadLocalConfig {
@Bean
@Scope("prototype")
public StatefulService statefulService() {
return new StatefulService();
}
@Bean
public ThreadLocalTargetSource statefulServiceTargetSource() {
ThreadLocalTargetSource targetSource = new ThreadLocalTargetSource();
targetSource.setTargetBeanName("statefulService");
targetSource.setBeanFactory(applicationContext);
return targetSource;
}
}
// Usage - each thread gets its own instance
ThreadLocalTargetSource targetSource = new ThreadLocalTargetSource();
targetSource.setTargetBeanName("myStatefulBean");
targetSource.setBeanFactory(beanFactory);
ProxyFactory factory = new ProxyFactory();
factory.setTargetSource(targetSource);
MyStatefulService proxy = (MyStatefulService) factory.getProxy();
// Each thread will get its own instance
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
proxy.doSomething(); // Each thread gets its own target instance
System.out.println("Thread: " + Thread.currentThread().getName() +
", Target: " + System.identityHashCode(proxy.getCurrentState()));
});
}
// Check statistics
System.out.println("Total invocations: " + targetSource.getInvocationCount());
System.out.println("Cache hits: " + targetSource.getHitCount());
System.out.println("Objects created: " + targetSource.getObjectCount());@Configuration
public class PoolingConfig {
@Bean
@Scope("prototype")
public DatabaseConnection databaseConnection() {
return new DatabaseConnection();
}
@Bean
public CommonsPool2TargetSource databaseConnectionPool() {
CommonsPool2TargetSource targetSource = new CommonsPool2TargetSource();
targetSource.setTargetBeanName("databaseConnection");
targetSource.setBeanFactory(applicationContext);
// Pool configuration
targetSource.setMaxSize(20);
targetSource.setMaxIdle(10);
targetSource.setMinIdle(2);
targetSource.setMaxWait(5000); // 5 seconds max wait
targetSource.setTestOnBorrow(true);
targetSource.setTestOnReturn(true);
targetSource.setTimeBetweenEvictionRunsMillis(30000); // 30 seconds
targetSource.setMinEvictableIdleTimeMillis(60000); // 1 minute
return targetSource;
}
@Bean
public ProxyFactoryBean databaseConnectionProxy() {
ProxyFactoryBean proxyFactory = new ProxyFactoryBean();
proxyFactory.setTargetSource(databaseConnectionPool());
proxyFactory.setInterfaces(DatabaseConnection.class);
proxyFactory.addAdvice(new PoolMonitoringInterceptor());
return proxyFactory;
}
}
// Custom monitoring interceptor for pool statistics
public class PoolMonitoringInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TargetSource targetSource =
((Advised) invocation.getThis()).getTargetSource();
if (targetSource instanceof PoolingConfig) {
PoolingConfig pool = (PoolingConfig) targetSource;
System.out.println("Pool stats - Active: " + pool.getActiveCount() +
", Idle: " + pool.getIdleCount() +
", Max: " + pool.getMaxSize());
}
return invocation.proceed();
}
}public class FileBasedRefreshableTargetSource extends AbstractRefreshableTargetSource {
private String configFilePath;
private long lastModified = -1;
public void setConfigFilePath(String configFilePath) {
this.configFilePath = configFilePath;
}
@Override
protected boolean requiresRefresh(Object cachedTarget, long refreshCheckDelay) {
File configFile = new File(configFilePath);
if (!configFile.exists()) {
return false;
}
long currentModified = configFile.lastModified();
if (currentModified != lastModified) {
lastModified = currentModified;
return true;
}
return false;
}
@Override
protected Object freshTarget() {
try {
// Read configuration and create new target
Properties config = new Properties();
config.load(new FileInputStream(configFilePath));
ConfigurableService service = new ConfigurableService();
service.configure(config);
return service;
} catch (IOException e) {
throw new RuntimeException("Failed to refresh target from config file", e);
}
}
}
// Usage
FileBasedRefreshableTargetSource refreshableSource = new FileBasedRefreshableTargetSource();
refreshableSource.setConfigFilePath("/path/to/config.properties");
refreshableSource.setTargetBeanName("configurableService");
refreshableSource.setBeanFactory(beanFactory);
ProxyFactory factory = new ProxyFactory();
factory.setTargetSource(refreshableSource);
factory.addInterface(ConfigurableService.class);
ConfigurableService proxy = (ConfigurableService) factory.getProxy();
// Service will automatically reload when config file changes
proxy.doWork(); // Uses current configuration
// Manually refresh if needed
refreshableSource.refresh();
// Check refresh statistics
System.out.println("Refresh count: " + refreshableSource.getRefreshCount());
System.out.println("Last refresh: " + refreshableSource.getLastRefreshTime());public class FailoverTargetSource implements TargetSource {
private final List<TargetSource> targetSources;
private int currentIndex = 0;
private final CircuitBreaker circuitBreaker;
public FailoverTargetSource(List<TargetSource> targetSources) {
this.targetSources = targetSources;
this.circuitBreaker = new CircuitBreaker(5, Duration.ofMinutes(1));
}
@Override
public Class<?> getTargetClass() {
return targetSources.get(0).getTargetClass();
}
@Override
public boolean isStatic() {
return false;
}
@Override
public Object getTarget() throws Exception {
for (int i = 0; i < targetSources.size(); i++) {
int index = (currentIndex + i) % targetSources.size();
TargetSource targetSource = targetSources.get(index);
try {
if (circuitBreaker.canExecute(index)) {
Object target = targetSource.getTarget();
currentIndex = index; // Update to successful source
circuitBreaker.recordSuccess(index);
return target;
}
} catch (Exception e) {
circuitBreaker.recordFailure(index);
// Try next target source
}
}
throw new Exception("All target sources failed");
}
@Override
public void releaseTarget(Object target) throws Exception {
// Release to all sources that might own this target
for (TargetSource targetSource : targetSources) {
try {
targetSource.releaseTarget(target);
} catch (Exception e) {
// Ignore release failures
}
}
}
private static class CircuitBreaker {
private final Map<Integer, AtomicInteger> failureCounts = new ConcurrentHashMap<>();
private final Map<Integer, Long> lastFailureTime = new ConcurrentHashMap<>();
private final int threshold;
private final Duration timeout;
public CircuitBreaker(int threshold, Duration timeout) {
this.threshold = threshold;
this.timeout = timeout;
}
public boolean canExecute(int sourceIndex) {
AtomicInteger failures = failureCounts.getOrDefault(sourceIndex, new AtomicInteger(0));
Long lastFailure = lastFailureTime.get(sourceIndex);
if (failures.get() >= threshold && lastFailure != null) {
return System.currentTimeMillis() - lastFailure > timeout.toMillis();
}
return true;
}
public void recordSuccess(int sourceIndex) {
failureCounts.put(sourceIndex, new AtomicInteger(0));
lastFailureTime.remove(sourceIndex);
}
public void recordFailure(int sourceIndex) {
failureCounts.computeIfAbsent(sourceIndex, k -> new AtomicInteger(0)).incrementAndGet();
lastFailureTime.put(sourceIndex, System.currentTimeMillis());
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-springframework--spring-aop