I'm trying to switch datasource from c3p0 to Tomcat JNDI in a Spring Boot 1.1.6 web project. I've found a sample application in GitHub, which works fine when DataSource instance is accessed from @RestController
annotated class.
@RestController
public class TestController {
@Autowired
private DataSource dataSource;
@RequestMapping("/test")
@ResponseBody
public String test() {
// Gets object instance... everything is OK...
System.out.println(this.dataSource);
}
However, when I try to inject same datasource to @Service
annotated bean, I get javax.naming.NameNotFoundException
as soon as instance is used in code.
@Service
public class TestService {
@Autowired
private DataSource dataSource;
@PostConstruct
private void init() {
// Throws exception...
System.out.println(this.dataSource);
}
Stack trace:
Caused by: javax.naming.NameNotFoundException: Name [java:comp/env/jdbc/myDataSource] is not bound in this Context. Unable to find [java:comp].
at org.apache.naming.NamingContext.lookup(NamingContext.java:819)
at org.apache.naming.NamingContext.lookup(NamingContext.java:167)
at javax.naming.InitialContext.lookup(InitialContext.java:411)
at org.springframework.jndi.JndiTemplate$1.doInContext(JndiTemplate.java:155)
at org.springframework.jndi.JndiTemplate.execute(JndiTemplate.java:87)
at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:152)
at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:179)
at org.springframework.jndi.JndiLocatorSupport.lookup(JndiLocatorSupport.java:95)
at org.springframework.jndi.JndiObjectLocator.lookup(JndiObjectLocator.java:106)
at org.springframework.jndi.JndiObjectTargetSource.getTarget(JndiObjectTargetSource.java:135)
... 11 more
I wonder why JNDI datasource bean can not be accessed from @Service class ? Any ideas ?
The actual issue is that you are trying to get the datasource within PostConstruct
, and not that it fails specifically in service beans.
By default, Tomcat uses the thread context class loader to determine which JNDI context a lookup should be performed against. So when you're binding the resource into the web app's JNDI context, you need to ensure that the lookup is performed when the web app's class loader (i.e. org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader
) is the thread context class loader.
Following is the snippet of code for org.apache.naming.ContextBindings.getClassLoader
which performs the check mentioned above:
/**
* Retrieves the naming context bound to a class loader.
*/
public static Context getClassLoader()
throws NamingException {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Context context = null;
do {
context = clBindings.get(cl);
if (context != null) {
return context;
}
} while ((cl = cl.getParent()) != null);
throw new NamingException
(sm.getString("contextBindings.noContextBoundToCL"));
}
So, when the code is within PostConstruct
and you perform the JNDI lookup (which is done implicitly when you access the object) the thread context class loader is still the application class loader (i.e. sun.misc.Launcher$AppClassLoader
). This is the reason why JNDI is not able to find the object since it is trying to find java:comp/env/jdbc/myDataSource
naming context (i.e. javax.naming.Context
) from sun.misc.Launcher$AppClassLoader
.
You can move code from PostConstruct
and it should work perfectly fine.
Crosslinking another related question, which also provides more insight in the issue.
I hope this answers your question.