Debugging Environment
Before diving into the execution flow, ensure you have access to the following project for reference:
- Sample project: https://github.com/1367356/laboratoryWeb
- Simple Spring AOP implementation: SpringAOPTheory
Start the application and navigate to: http://localhost:9002/queryNews?htmlid=1531872732684
Step 1: Controller Entry Point
The request enters the controller layer. Set a breakpoint at the service call:
News news = foreService.queryNews(htmlid);
At this point, foreService is already a proxy object wrapping the actual service implementation. This proxy is created by Spring's AOP framework and contains the logic for intercepting method invocations.
Step 2: Service Layer Logging
When stepping into the service implementation, you can observe logging operations:
logger.debug(htmlid);
This leads to the underlying logging mechanism:
public void debug(final Object message) {
logIfEnabled(FQCN, Level.DEBUG, null, message, null);
}
Step 3: Mapper Proxy Invocation
The service layer calls the mapper, which is also a proxy object:
return foreMapper.queryNews(htmlid);
At this stage, foreMapper is a MapperProxy instance that implements the InvocationHandler inteerface. This proxy contains references to the SqlSession and holds the SQL statement definitions from the corresponding Mapper XML file.
Step 4: JDK Dynamic Proxy Execution
The call enters the JDK dynamic proxy's invoke method, specifically the JdkDynamicAopProxy class. This is where the AOP invocation chain is constructed and executed.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Class<?> targetClass = null;
Object target = null;
try {
// Handle special methods like equals and hashCode
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
return equals(args[0]);
} else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
return hashCode();
} else if (method.getDeclaringClass() == DecoratingProxy.class) {
return AopProxyUtils.ultimateTargetClass(this.advised);
} else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}
Object retVal;
// Expose proxy if configured
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Obtain target instance
target = targetSource.getTarget();
if (target != null) {
targetClass = target.getClass();
}
// Retrieve interceptor chain for this method
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(
method, targetClass);
// Execute directly if no interceptors exist
if (chain.isEmpty()) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
} else {
// Create method invocation with interceptor chain
invocation = new ReflectiveMethodInvocation(
proxy, target, method, args, targetClass, chain);
retVal = invocation.proceed();
}
// Handle return value if it is 'this'
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
retVal = proxy;
} else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
} finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
AopContext.setCurrentProxy(oldProxy);
}
}
}
Step 5: Obtaining the Interceptor Chain
The framework retrieves all interceptors and advisors associated with this method:
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(
method, targetClass);
This chain contains all the advice (before, after, around, etc.) that should be applied to the method invocation.
Step 6: Creating the Method Invocation Objecct
Since the interceptor chain is not empty, a ReflectiveMethodInvocation object is created:
invocation = new ReflectiveMethodInvocation(
proxy, target, method, args, targetClass, chain);
retVal = invocation.proceed();
This invocation object encapsulates all the information needed to proceed through the interceptor chain and eventually execute the target method.
Step 7: Processing the Interceptor Chain
The ReflectiveMethodInvocation.proceed() method iterative processes each interceptor in the chain:
@Override
public Object proceed() throws Throwable {
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
} else {
return proceed();
}
} else {
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
Each interceptor is responsible for calling proceed() to continue the chain, eventually reaching the target method.
Step 8: Persistence Exception Translation
One common interceptor in Spring applications is the PersistenceExceptionTranslationInterceptor:
public class PersistenceExceptionTranslationInterceptor
implements MethodInterceptor, BeanFactoryAware, InitializingBean {
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
} catch (RuntimeException ex) {
if (!this.alwaysTranslate &&
ReflectionUtils.declaresException(mi.getMethod(), ex.getClass())) {
throw ex;
} else {
if (this.persistenceExceptionTranslator == null) {
this.persistenceExceptionTranslator =
detectPersistenceExceptionTranslators(this.beanFactory);
}
throw DataAccessUtils.translateIfNecessary(ex,
this.persistenceExceptionTranslator);
}
}
}
}
This interceptor translates database exceptions into Spring's unified data access exception hierarchy.
Step 9: Invoking the Joinpoint
When all interceptors have been processed, the actual target method is invoked through reflection:
protected Object invokeJoinpoint() throws Throwable {
return AopUtils.invokeJoinpointUsingReflection(
this.target, this.method, this.arguments);
}
Step 10: Reflection-Based Method Invocation
The AopUtils class handles the actual method invocation:
public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args)
throws Throwable {
try {
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
} catch (IllegalArgumentException ex) {
throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
method + "] on target [" + target + "]", ex);
} catch (IllegalAccessException ex) {
throw new AopInvocationException("Could not access method [" + method + "]", ex);
}
}
The makeAccessible method ensures the method can be invoked even if it's not public:
public static void makeAccessible(Method method) {
if ((!Modifier.isPublic(method.getModifiers()) ||
!Modifier.isPublic(method.getDeclaringClass().getModifiers())) &&
!method.isAccessible()) {
method.setAccessible(true);
}
}
Step 11: Entering the MyBatis Mapper Proxy
The call now enters the MyBatis MapperProxy:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
Step 12: Caching Mapper Methods
Each mapper method is cached to avoid repeated instantiation:
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method,
sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
Step 13: Configuration Retrieval
The MapperMethod constructor receives the MyBatis configuration:
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
The configuration contains all SQL statement mappings defined in Mapper XML files.
Step 14: SQL Command Resolution
The SqlCommand class resolves the actual SQL statement:
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName,
declaringClass, configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
Step 15: Executing the SQL Statement
The MapperMethod.execute() method routes to the appropriate SQL operation:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ "' attempted to return null from a method with a primitive return type ("
+ method.getReturnType() + ").");
}
return result;
}
For a query operation like queryNews(), the flow reaches sqlSession.selectOne(), which executes the actual database query and returns the result.
Summary
This debugging journey reveals the complete execution flow of a Spring AOP proxied method call. The request travels through multiple layers: controller → service proxy → interceptor chain → target method → mapper proxy → SQL execution. Each layer adds its own logic through the proxy mechanism, enabling powerful cross-cutting concerns like transaction management, logging, and security to be applied transparently.