I am learning how to implement a Tomcat-like server and I try to apply Spring AOP
into this project. And this the exception I got when I tried to point my advices to a method by aop
:
WARNING: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'service' defined in URL [jar:file:/Users/chaozy/Desktop/CS/projects/java/TomcatDIY/lib/TomcatDIY.jar!/uk/ac/ucl/catalina/conf/Service.class]:
Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [uk.ac.ucl.catalina.conf.Service]: Constructor threw exception;
nested exception is java.lang.ClassCastException: class com.sun.proxy.$Proxy31 cannot be cast to class uk.ac.ucl.catalina.conf.Connector (com.sun.proxy.$Proxy31 and uk.ac.ucl.catalina.conf.Connector are in unnamed module of loader uk.ac.ucl.classLoader.CommonClassLoader @78308db1)
So this is the Bootstrap::main
where I set the CommonClassLoader
to the primary class loader:
public static void main(String[] args)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
CommonClassLoader commonClassLoader = new CommonClassLoader();
Thread.currentThread().setContextClassLoader(commonClassLoader);
// Invoke the init() method in class Server
Class<?> serverClass = commonClassLoader.loadClass("uk.ac.ucl.catalina.conf.Server");
Constructor<?> constructor = serverClass.getConstructor();
Object serverObject = constructor.newInstance();
Method m = serverClass.getMethod("init");
m.invoke(serverObject);
}
This is the Server::init
method, which uses Spring to handle Service
class.
public class Server{
private void init() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
service = ApplicationContextHolder.getBean("service");
service.start();
}
}
This is the Service::start
method, the connectors
in the method are also generated by Spring.
public class Service{
public void start() {
for (Connector connector : connectors) {
connector.setService(this);
connector.init(connector.getPort());
}
}
}
This is my advice
:
@Before("execution(void uk.ac.ucl.catalina.conf.Connector.init(..))")
public void initConnector(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
int port = (int)args[0];
Logger logger = LogManager.getLogger("ServerXMLParsing");
logger.info("Initializing ProtocolHandler [http-bio-{}]", port);
}
The pointcut
is located at one of the methods in the class Connector
, which is loaded in by a custom classloader CommonClassLoader
(implement java.lang.ClassLoader
).
I didn't find many similar questions online. One might be useful if The top answer of this post, which says The author's analysis is correct as the JarClassLoader must be the primary classloader of the current thread.
But I am not sure if my problem is the same as that one.
In my case the default classloader would be ApplicationClassLoader
if I don't use a custom one. So does it mean I have to use the default classloader if I want to apply spring aop
?
UPDATE
I put System.out.println(serverClass.getClassLoader());
in the BootStrap::main
method and it showed uk.ac.ucl.classLoader.CommonClassLoader@78308db1
. And same for Connector
class.
Here is the CommonClassLoader
, it adds all of the jars
under /lib
to the url list of files and resources. This includes a file which packed all of the compiled .classes
.
public class CommonClassLoader extends URLClassLoader {
public CommonClassLoader() {
super(new URL[]{});
File workDir = new File(System.getProperty("user.dir"));
File libDir = new File(workDir, "lib");
File[] jarFiles = libDir.listFiles();
for (File file : jarFiles) {
if (file.getName().endsWith(".jar")){
try {
URL url = new URL("file:" + file.getAbsolutePath());
this.addURL(url);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
}
}
In order to make the classes loaded by my own classloader instead of applicationClassLoader
. Only Bootstrap
and CommonClassLoader
are needed to start the server, this two classes will be loaded by ApplicationClassLoader
, the others will be loaded by CommonClassLoader
. this startup file is used:
rm -f bootstrap.jar
jar cvf0 bootstrap.jar -C target/classes uk/ac/ucl/Bootstrap.class -C target/classes uk/ac/ucl/classLoader/CommonClassLoader.class
rm -f lib/TomcatDIY.jar
cd target/classes
jar cvf0 ../../lib/MyTomcat.jar *
cd ..
cd ..
java -cp bootstrap.jar uk.ac.ucl.Bootstrap
I can reproduce your problem with the second GitHub repo and your information how to start the server. So thank you for the MCVE. 😀
You do not have a class-loading problem like we both suspected. The explanation is much simpler: You have a Spring AOP configuration problem, a typical beginner's mistake.
Looking at this class
@Component
@Scope("prototype")
@Setter @Getter
public class Connector implements Runnable {
// (...)
}
we see that this is a class implementing an interface. The default for Spring AOP auto-proxying is that it uses JRE dynamic proxies, i.e. when creating the bean like this
Connector connector = ApplicationContextHolder.getBean("connector");
Spring will create a dynamic proxy implementing all interfaces the target class also implements. In this case this is Runnable
only. I.e. the proxy object will only have methods from that interface and can only be cast to that interface. This explains why you cannot cast the proxy to Connector
.
If you want Spring to create proxies for classes directly via CGLIB, you need to change your configuration in beans.xml
to
<aop:aspectj-autoproxy proxy-target-class="true"/>
Then the server will start up normally because the CGLIB proxy is a direct Connector
subclass, which also means that the assignment works as intended.
See the Spring manual for more information.
Epilogue & lesson learned: This question is a perfect example for why an MCVE is so much better and more powerful than just a set of incoherent code snippets without build files, configuration, package names, imports etc.:
beans.xml
with the auto-proxy configuration which was absolutely vital for reproducing the problem.class ...$Proxy31 cannot be cast to class ...Connector
if I had had the Connector
class at my disposal, seeing that it actually is a class and not an interface and concluding from the typical $Proxy[number]
class name that this was a JDK dynamic proxy because CGLIB proxies have a name like Connector$$EnhancerByCGLIB$$[number]
. But just seeing the source code and not being able to run it, chances are that I would have overlooked this subtle piece of information, my focus being the custom class loader. My brain is not a JVM, after all.So when asking questions on SO or looking for debugging help as a software developer in general, always try to make the problem reproducible for your helpers by means of an MCVE. You might think you know where approximately the problem is and even provide some plausible explanation with your own biased selection of shared information. But you could be wrong and make things worse by also creating the same bias in your helpers' minds, unintentionally further obscuring the real problem and probably lengthening instead of shortening the search for a solution.
Bottom line: An MCVE is an MCVE is an MCVE - and MCVEs rule! Preparing an MCVE is not, as many people refusing to do so like to think, a waste of time and effort, but in most cases saves a ton of time and even makes the difference between solving the problem or being stuck forever.