0

I want to implement a kind of "plugin mechanism" using EJB3. I have two+ war's, each containing own bean types using Remote Interfaces defined in a separate project. Basically, I want my main Bean (Product) to always be deployed and provide a mechanism for other Beans (n different Project Beans) to register against. It is important that arbitrary Beans may register, as long as they have knowledge of the Product's Remote Interface. Sample code:

Product Bean

@Singleton
@ConcurrencyManagement(BEAN)
public class ProductBean implements ProductRemote, Serializable {
    private static final long serialVersionUID = 4686943363874502919L;

    private ProjectRemote project;

    public void registerProject(ProjectRemote project) {
        this.project = project;
    }

    public void something() {
        if(project != null)
            project.doSomething();
    }

}

Project Bean

@ConcurrencyManagement(BEAN)
@Singleton
@Startup
public class ProjectBean implements ProjectRemote, Serializable {
    private static final long serialVersionUID = -2034521486195622039L;

    @EJB(lookup="java:global/product/ProductBean")
    private ProductRemote product;

    @PostConstruct
    private void registerSelf() {
        product.registerProject(this);
    }

    public void doSomething() {
        System.err.println("FOOBAR");
    }
}

Product Interface

@Remote
public interface ProductRemote {
    public void registerProject(ProjectRemote project);
}

Project Interface

@Remote
public interface ProjectRemote {
    public void doSomething();
}

Unfortunately, when I try to deploy the Project I get a ClassNotFoundException:

22:56:33,146 ERROR [org.jboss.as.controller.management-operation] (XNIO-1 task-7) JBAS014613: Operation ("add") failed - address: ([{"deployment" => "project-0.1-SNAPSHOT.war"}]) - failure description: {"JBAS014671: Failed services" => {"jboss.deployment.unit.\"project-0.1-SNAPSHOT.war\".component.ProjectBean.START" => "org.jboss.msc.service.StartException in service jboss.deployment.unit.\"project-0.1-SNAPSHOT.war\".component.ProjectBean.START: java.lang.IllegalStateException: JBAS011048: Failed to construct component instance
Caused by: java.lang.IllegalStateException: JBAS011048: Failed to construct component instance
Caused by: javax.ejb.EJBException: java.lang.RuntimeException: JBAS014154: Failed to marshal EJB parameters
Caused by: java.lang.RuntimeException: JBAS014154: Failed to marshal EJB parameters
Caused by: java.lang.ClassNotFoundException: com.xxx.test.project.ProjectBean from [Module \"deployment.product-0.1-SNAPSHOT.war:main\" from Service Module Loader]"}}

So my question is: is there any way to realize such a functionality? Or is it entirely impossible because of the different classpaths for each war?

Kubus
  • 23
  • 6

2 Answers2

0

I think the first problem is that even though you're sharing your interface, the client implementation class is still not available with the other project, and it's instance was loaded with a different classloader (ie, another war container).

You could implement things using a message bean (JMS) design, which will allow separation of concerns, a quasi-registration (to a queue/topic), and allow you to deploy any implementation you want. Check out how I'm working with JMS and ACtiveMQ.

Another solution would be to use a delegate pattern, as such:

Assuming that you went from two separate war deployments to a single EAR deployment, you're project will have the following modules.

myLibrary

EJB interface:

@Local
public interface MyBeanLocal
    public void doSomething(String something);

MyClient annotation:

@Documented
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface MyClient {

}

MyDefault annotation:

@Documented
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface MyDefault {

}

myClientEJB

@Stateless
@MyClient
public class MyBean implements MyBeanLocal {
@Inject
@MyDefault
private MyBeanLocal myDefault;

@Override
public void doSomething(String something) {
    myDefault.doSomething(something);
}
}

myDefaultEJB

@Stateless
@MyDefault
public class MyBean implements MyBeanLocal {

@Override
public void doSomething(String something) {
    // do something
}
}

myWeb

@Named
@Request
public class MyRequestBean {

@Inject
@MyClient
private MyBeanLocal myClient;

public void something() {
    myClient.doSomething("...");
}
}

Lastly, you might want to consider OSGi. I haven't done anything with that, but it might be what you're looking for as far registering resources. Of course, you could always consider a resource adapter depending on your problem domain.

Community
  • 1
  • 1
John Manko
  • 1,828
  • 27
  • 51
0

A colleague of mine gave me the right hint about what I did wrong in my code - the way I wrote it I was trying to get my implementation injected. Obviously, this does not work because of the different classpaths (and JVMs). Instead, I had to pass the reference to the proxy object created for the remote interface into the registration method.

So, in order to get the above code working, two changes hav been made to Project.java:

  1. Add an EJB for itself:

    @EJB
    private ProjectRemote self;
    
  2. Change the @PostConstruct method to reference the EJB

    @PostConstruct
    private void registerSelf() {
        product.registerProject(self);
    }
    

This way, the actual implementation of the class does not need to be known by the class that is using it. The container knows where to pass the information based on the proxy object created for the EJB.

Kubus
  • 23
  • 6