How to use JPA2 on JBoss 5.x ? (or How to eliminate class loading isolation issue?)
Asked Answered
P

3

8

I would like JBoss to use only the dependencies located in my war file. Each time I deploy this war file, JBoss still uses its own jars.

Here is the jboss-web.xml I use :

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
    <class-loading java2ClassLoadingCompliance="false">
        <loader-repository>
            my.package:loader=my-app.war
           <loader-repository-config>
              java2ParentDelegation=false
           </loader-repository-config>
        </loader-repository>
    </class-loading>
</jboss-web>

and the jboss-classloading.xml :

<?xml version="1.0" encoding="UTF-8"?>
<classloading 
    xmlns="urn:jboss:classloading:1.0"
    export-all="NON_EMPTY"
    import-all="true"
    parent-first="false"/>

JBoss 5.1.0.GA

Paraph answered 12/12, 2011 at 15:49 Comment(5)
How do you know it is using it's own jars?Enriquetaenriquez
@Enriquetaenriquez When my app run , I got a NoSuchMethod exception.Paraph
Is the WAR deployed on its own or is it part of an EAR?Kestrel
@Kestrel the WAR is deployed on its ownParaph
@Paraph - This link should be helpful if you are attempting to use JPA2 with JBoss 5.x.Kestrel
P
8

1> SUMMARY

Initially, I have tried this class loading isolation for loading Hibernate 3.6.4 jars with JBoss 5.1.0.GA.

It's definitively NOT possible. There is some magic under the hood that prevents you from using any Hibernate version with JPA2 support.

I'm really disappointed that JBoss project didn't provide some kind of patch or service pack for supporting JPA2 on 5.1.0.GA.

2> WORKAROUND : "The Kernel solution"
I have managed to use JPA2 with JBoss 5.1.0.GA I describe here my recipe. It's more a proof of concept you can use to make your own solution.

Ingredients :

  • 1 WAR archive
  • 1 servlet
  • 1 standalone java application (J2SE)

Recipe :

Step 1: Build the standalone application (APP)

This application will receive instructions from the servlet for using Hibernate.

I leave you the choice of the communication method. As the APP uses JPA2, it will need a persistence.xml file located in a META-INF folder. Since JBoss 5.x, when you deploy a WAR, JBoss will scan the WAR and all its sub-deployments for finding and deploying blindly persistence.xml files. Rename your persistence.xml file into my-persistence.xml for example. Use the code below when you build your EntityManagerFactory (Prevent JBoss from deploying persistence.xml).

UPDATE: This method does work but some strange warnings are raised by Hibernate. In order to stop those warnings, I have decided to put the META-INF folder and the persistence file (renamed back to persistence.xml now) outside of the WAR. In my case, I choosed a special config folder on the hard drive and added it to the classpath. No more strange warnings and no custom classloader required for loading the persistence file.

I leave it up to you to choose between using a custom class loader or changing the persistence file location. In both cases, JBoss won't find the persistence file.


Step 2: Build the servlet

When the servlet needs to access the database, it launches the APP and tells it what to do.

For lauching the APP, the servlet is responsible of spawning a new JVM and build the classpath of the APP. Read the code below for (Spawning a JVM). The classpath is easily buildable since all the required jars will be in the /lib directory of the WAR archive...


Step 3: Build the WAR archive

Build a WAR archive where you put the servlet and the standalone application packaged as a JAR. The APP will be a dependency of the WAR.


Prevent JBoss from deploying persistence.xml

// Install a proxy class loader for adding renamed persistence.xml file
Thread t = Thread.currentThread();
ClassLoader clOriginal = t.getContextClassLoader();
t.setContextClassLoader(new SpecialClassLoader(clOriginal, "META-INF/my-persistence.xml"));

// Build EntityManagerFactory
EntityManagerFactory emf = Persistence.createEntityManagerFactory(persistenceUnitName);

// Restore original class loader
t.setContextClassLoader(clOriginal);

//...

private class ProxyClassLoader extends ClassLoader {
    private ClassLoader realClassLoader;
    private String hiddenFromJBossPersistenceFile;

    public ProxyClassLoader(ClassLoader realClassLoader, String hiddenFromJBossPersistenceFile) {
        this.realClassLoader = realClassLoader;
        this.hiddenFromJBossPersistenceFile = hiddenFromJBossPersistenceFile;
    }

    public void clearAssertionStatus() {
        realClassLoader.clearAssertionStatus();
    }

    public boolean equals(Object obj) {
        return realClassLoader.equals(obj);
    }

    public URL getResource(String name) {
        return realClassLoader.getResource(name);
    }

    public InputStream getResourceAsStream(String name) {
        return realClassLoader.getResourceAsStream(name);
    }

    public Enumeration<URL> getResources(String name) throws IOException {
        ArrayList<URL> resources = new ArrayList<URL>();

        if (name.equalsIgnoreCase("META-INF/persistence.xml")) {
            resources.add(getResource(this.hiddenFromJBossPersistenceFile));
        }
        resources.addAll(Collections.list(realClassLoader.getResources(name)));

        return Collections.enumeration(resources);
    }

    public int hashCode() {
        return realClassLoader.hashCode();
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return realClassLoader.loadClass(name);
    }

    public void setClassAssertionStatus(String className, boolean enabled) {
        realClassLoader.setClassAssertionStatus(className, enabled);
    }

    public void setDefaultAssertionStatus(boolean enabled) {
        realClassLoader.setDefaultAssertionStatus(enabled);
    }

    public void setPackageAssertionStatus(String packageName, boolean enabled) {
        realClassLoader.setPackageAssertionStatus(packageName, enabled);
    }

    public String toString() {
        return realClassLoader.toString();
    }
}

Spawning a JVM

public static Process createProcess(final String optionsAsString, final String workingDir, final String mainClass, final String[] arguments) throws IOException {
    String jvm = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";

    String[] options = optionsAsString.split(" ");
    List<String> command = new ArrayList<String>();
    command.add(jvm);
    command.addAll(Arrays.asList(options));
    command.add(mainClass);
    command.addAll(Arrays.asList(arguments));

    //System.out.println(command);

    ProcessBuilder processBuilder = new ProcessBuilder(command);
    processBuilder.directory(new File(workingDir));

    return processBuilder.start();
}

public static void makeItRun() {
   try {
      // Start JVM
      String classPath = buildClassPath();
      String workingDir = getSuitableWorkingDir();//or just "."
      Process java = createProcess("-cp \"" + classPath + "\"", workingDir, my.package.APP.class.getCanonicalName(), "-the -options -of -my -APP");

      // Communicate with your APP here ...

      // Stop JVM
      java.destroy();
   } catch(Throwable t) {
      t.printStackTrace();
   }
}
Paraph answered 13/12, 2011 at 9:54 Comment(0)
F
1

The original configuration you had posted (jboss-web.xml and jboss-classloading.xml) works for me on EAP-5.12.

Apparently the jboss-web.xml's

java2ClassLoadingCompliance="false"

and/or

    <loader-repository-config> 
      java2ParentDelegation=false 
   </loader-repository-config>

are being ignored by Jboss. See http://docs.jboss.org/jbossesb/docs/4.10/manuals/html/Getting_Started_Guide/index.html#sect-scoped_deploy where this is mentioned (community edition, but apparently also valid for the blessed edition of jboss)

I had been struggling w/ this for a long time. Then gave up. Then recently revisited it and stumbled on the jboss-classloading.xml solution.

Without it, I would get ClassCastException:

java.lang.ClassCastException: org.hibernate.ejb.HibernatePersistence cannot be cast to javax.persistence.spi.PersistenceProvider

I have a later version (3.6.0.Final) of hibernate-entitymanager in the war than what is used by Jboss.

    <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-entitymanager</artifactId>
   <version>3.6.0.Final</version>
   <type>jar</type>
   <scope>compile</scope>
</dependency>

What's great about this, now that it works, is I can deploy same war on tomcat and jboss. (tomcat in the cloud for a backup solution, and jboss on the "ground".)

Ferity answered 28/7, 2012 at 17:14 Comment(1)
Great if it works for you on JBoss EAP-5.1.2 but initially I had no choice to work with JBoss 5.1.0.GA ;)Paraph
H
1

Agreed. We took the following steps:

The following JAR files need to be removed from the jboss_home/common/lib directory:

  • ejb3-persistence.jar (we will be using Hibernate's implementation for JPA 2.0)
  • hibernate-annotations.jar (this is now part of hibernate-core)
  • hibernate-c3p0.jar hibernate-commons-annotations.jar
  • hibernate-core.jar hibernate-entitymanager.jar
  • hibernate-validator.jar

Add the following upgraded JARs to jboss_home/common/lib directory:

  • hibernate-c3p0-3.6.10.Final.jar
  • hibernate-commons-annotations-3.2.0.Final.jar
  • hibernate-core-3.6.10.Final.jar
  • hibernate-entitymanager-3.6.10.Final.jar
  • hibernate-jpa-2.0-api-1.0.1.Final.jar
  • hibernate-validator-4.2.0.Final.jar
  • validation-api-1.0.0.GA.jar (dependency of hibernate-validator)

Add file (e.g.) my-war\src\main\webapp\WEB-INF\jboss-classloading.xml

Override the default class-loading policy to ensure the hibernate classes in your war are used in lieu of any JBoss client libraries. This can be done by adding a jboss-classloading.xml file to the 'my-war/src/main/webapp/WEB-INF' directory. The import line to note is setting the parent-first attribute to false.

<?xml version="1.0" encoding="UTF-8"?>
 <classloading xmlns="urn:jboss:classloading:1.0" 
   export-all="NON_EMPTY"  
   import-all="true"  
   parent-first="false">
 </classloading>

Rename (e.g.) '/src/main/resources/META-INF/persistence.xml'

JBoss will attempt to blindly load any persistence.xml file that exists in a META-INF directory on the classpath. Since the persistence unit metadata deployer shipped with JBoss only supports JPA 1.0, we need to disable this behavior and delegate it to Spring. This can easily be done by renaming the persistence.xml to (e.g.) /src/main/resources/META-INF/my-persistence.xml

Update '/src/main/resources/spring-jpaaccess.xml'

Update the Spring application context configuration (e.g.) '/src/main/resources/spring-jpaaccess.xml' and add persistenceXmlLocation as follows.

<bean id="entityManagerFactory" 
   class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> 
   <property name="persistenceUnitName" value="myPU" /> 
   <property name="persistenceXmlLocation"  value="classpath:META-INF/my-persistence.xml" />   
/bean>
Heresiarch answered 2/10, 2014 at 17:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.