Setting up a project with Spring and OSGi

Table of Contents

starting

The goal is to create a simple osgi service that will send emails using the spring framework.

The next lines create a new OSGi project with a bundle inside.

mvn org.ops4j:maven-pax-plugin:create-project -DgroupId=org.eknet.osgi.mailer -DartifactId=mailer -Dversion=0.0.1-SNAPSHOT
cd mailer
mvn pax:create-bundle -Dpackage=org.eknet.osgi.mailer

The service should send SimpleMailMessages using Spring's API. The spring mail package is in the artifact "spring-context-support". This needs to be added to the bundle:

cd org.eknet.osgi.mailer
mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-context-support -Dversion=3.0.0.M3

This bundle itself needs now some dependencies which must be imported, too. I used the pax:provision command to start the OSGi platform and to find out the missing dependencies.

mvn install pax:provision

Then I looked up the dependencies from the springsource repository you find here: http://www.springsource.com/repository/app/. So the following bundles had to be imported:

mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-context -Dversion=3.0.0.M3
mvn pax:import-bundle -DgroupId=org.aopalliance -DartifactId=com.springsource.org.aopalliance -Dversion=1.0.0
mvn pax:import-bundle -DgroupId=org.apache.commons -DartifactId=com.springsource.org.apache.commons.logging -Dversion=1.1.1
mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-aop -Dversion=3.0.0.M3
mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-beans -Dversion=3.0.0.M3
mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-core -Dversion=3.0.0.M3
mvn pax:import-bundle -DgroupId=javax.mail -DartifactId=com.springsource.javax.mail -Dversion=1.4.1
mvn pax:import-bundle -DgroupId=javax.activation -DartifactId=com.springsource.javax.activation -Dversion=1.1.1

creating simple service

Within the bundle org.eknet.osgi.mailer a simple service class is created which uses spring and java-mail to send an email. It consists of an interface ImailerService and its implementation:

package org.eknet.osgi.mailer.internal;

import org.eknet.osgi.mailer.IMailerService;
import org.eknet.osgi.mailer.Sender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;

public final class MailerServiceImpl implements IMailerService {
  @Override
  public void sendMailMessage(Sender sender, SimpleMailMessage message) {
    JavaMailSenderImpl mailsys = new JavaMailSenderImpl();
    mailsys.setHost("mail.host.org");
    mailsys.setPort(25);
    message.setFrom(sender.toString());
    mailsys.send(message);
  }
}

creating a test

At first I like to try this code using a simple test. A separate test bundle needs to be created which is not part of the mailer project. It is therefore not listed in the main pom.xml as a module. It is rather created manually:

mkdir -p bundle-tests/src/test/java

Then I created the following pom.xml file with the pax-exam dependencies:

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
    http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >

  <parent>
    <relativePath>../poms/compiled/</relativePath>
    <groupId>org.eknet.osgi.mailer.build</groupId>
    <artifactId>compiled-bundle-settings</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>

  <modelVersion>4.0.0</modelVersion>
  <groupId>org.eknet.osgi.mailer</groupId>
  <artifactId>bundle-tests</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.6</version>
    </dependency>
    <dependency>
      <groupId>org.eknet.osgi.mailer</groupId>
      <artifactId>org.eknet.osgi.mailer</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.ops4j.pax.exam</groupId>
      <artifactId>pax-exam</artifactId>
      <version>0.6.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.ops4j.pax.exam</groupId>
      <artifactId>pax-exam-container-default</artifactId>
      <version>0.6.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.ops4j.pax.exam</groupId>
      <artifactId>pax-exam-junit</artifactId>
      <version>0.6.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.ops4j.pax.exam</groupId>
      <artifactId>pax-exam-junit-extender-impl</artifactId>
      <version>0.6.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.ops4j.pax.url</groupId>
      <artifactId>pax-url-dir</artifactId>
      <version>0.4.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>javax.mail</groupId>
      <artifactId>com.springsource.javax.mail</artifactId>
      <version>1.4.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>3.0.0.RC2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Next is writing the test. The test class uses the OSGi API to retrieve the service.

package org.eknet.osgi.mailertest;

import org.eknet.osgi.mailer.IMailerService;
import org.eknet.osgi.mailer.Sender;
import org.junit.Test;
import static org.ops4j.pax.exam.CoreOptions.options;
import static org.ops4j.pax.exam.CoreOptions.equinox;
import static org.ops4j.pax.exam.CoreOptions.provision;
import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
import static org.junit.Assert.assertNotNull;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.Inject;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.junit.Configuration;
import org.ops4j.pax.exam.junit.JUnit4TestRunner;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import org.springframework.mail.SimpleMailMessage;

@RunWith(JUnit4TestRunner.class)
public class MailerServiceBundleTest {

    @Inject
    private BundleContext bundleContext;

    @Configuration
    public static Option[] configuration() {
        return options(equinox(), provision(
            mavenBundle().groupId("org.springframework").artifactId("spring-context-support"),
            mavenBundle().groupId("org.springframework").artifactId("spring-context"),
            mavenBundle().groupId("org.aopalliance").artifactId("com.springsource.org.aopalliance"),
            mavenBundle().groupId("org.apache.commons").artifactId("com.springsource.org.apache.commons.logging"),
            mavenBundle().groupId("org.springframework").artifactId("spring-aop"),
            mavenBundle().groupId("org.springframework").artifactId("spring-beans"),
            mavenBundle().groupId("org.springframework").artifactId("spring-core"),
            mavenBundle().groupId("javax.mail").artifactId("com.springsource.javax.mail"),
            mavenBundle().groupId("javax.activation").artifactId("com.springsource.javax.activation"),
            mavenBundle().groupId("org.eknet.osgi.mailer").artifactId("org.eknet.osgi.mailer").version("0.0.1-SNAPSHOT")
        ));
    }

    @Test
    public void bundleContextNotNullTest() {
        assertNotNull( bundleContext );
    }

    @Test
    public void sendMailToMe() throws Exception {
        System.out.println("=====>>> starting mailing <<<<========");
        IMailerService mailer = retrieveMailerService();
        Sender from = new Sender("name12", "mail@from.me");
        SimpleMailMessage message = new SimpleMailMessage();
        message.setSubject("Goethe");
        message.setText("Und werd ich zum Augenblicke sagen\nVerweile doch du bist so schön\nDann kannst du " +
                "mich in Fesseln schlagen\nDann will ich gern zugrunde gehn.");
        message.setTo("your@mail.com");
        mailer.sendMailMessage(from, message);
     }
     private IMailerService retrieveMailerService() throws InterruptedException {
        ServiceTracker tracker = new ServiceTracker(bundleContext, IMailerService.class.getName(), null);
        tracker.open();
        IMailerService service = (IMailerService) tracker.waitForService(5000);
        tracker.close();
        assertNotNull(service);
        return service;
    }
}

Running this test with mvn test will send a mail to the specified receiver.

spring extender

The bundle right now just uses the spring api to easily send emails. But the goal is to have spring inject dependent OSGi services. This is the work of spring-dm.

First the spring-extender must be imported to the project

mvn pax:import-bundle -DgroupId=org.springframework.osgi -DartifactId=spring-osgi-extender -Dversion=2.0.0.M1 -DimportTransitive -DwidenScope

Then remove the OSGi activator and create spring configuration in META-INF/spring.

mkdir -p org.eknet.osgi.mailer/src/main/resources/META-INF/spring/

I created two files, one for the application context of the bundle and one for OSGi.

mailer-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean id="theMailerService" class="org.eknet.osgi.mailer.internal.MailerServiceImpl">
  </bean>
</beans>

mailer-osgi.xml

<beans:beans xmlns="http://www.springframework.org/schema/osgi"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/osgi
        http://www.springframework.org/schema/osgi/spring-osgi.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <service ref="theMailerService"
        interface="org.eknet.osgi.mailer.IMailerService" />
</beans:beans>

Still some dependencies missing. The spring dm failed with

Exception in thread "SpringOsgiExtenderThread-1" java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext

The provision command mvn pax:provision -Dprofiles=log gives some information about the cause. It was a ClassNotFoundException, so I did this:

mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-asm -Dversion=3.0.0.RC2

and upgraded the spring deps to version 3.0.0.RC2. This will also get rid of the quartz/quartz/1.6.2 dependency.

consumer

Now it's time to create another bundle that consumes the mailer service - again using Spring. This is fairly easy

First create a new bundle:

mvn pax:create-bundle -Dpackage=org.eknet.osgi.mailerconsumer -Dversion=0.0.2

Then some dependency bundles have to be imported.

mvn pax:import-bundle -DgroupId=org.eknet.osgi.mailer -DartifactId=org.eknet.osgi.mailer -Dversion=0.0.2-SNAPSHOT
mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-context-support -Dversion=3.0.0.RC2
mvn pax:import-bundle -DgroupId=javax.mail -DartifactId=com.springsource.javax.mail -Dversion=1.4.1

Then in META-INF/spring the spring configuration is created:

consumer-context.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean class="org.eknet.osgi.mailerconsumer.internal.ConsumerServiceImpl"
            init-method="run" destroy-method="stop">
        <constructor-arg ref="theMailerService" />
    </bean>
</beans>

consumer-osgi.xml

<beans:beans xmlns="http://www.springframework.org/schema/osgi"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/osgi
        http://www.springframework.org/schema/osgi/spring-osgi.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <reference id="theMailerService" interface="org.eknet.osgi.mailer.IMailerService" />
</beans:beans>

The new service looks like this:

package org.eknet.osgi.mailerconsumer.internal;

import org.eknet.osgi.mailer.IMailerService;
import org.eknet.osgi.mailer.Sender;
import org.eknet.osgi.mailerconsumer.ConsumerService;
import org.springframework.mail.SimpleMailMessage;

/**
 * Internal implementation.
 */
public final class ConsumerServiceImpl implements ConsumerService {
    private IMailerService mailerService;

    public ConsumerServiceImpl(IMailerService mailerService) {
        this.mailerService = mailerService;
    }

    public String consumeIt() {
        Sender from = new Sender("Name12", "abc@xyz.com");
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo("mail@yours.com");
        message.setSubject("Hello from Spring and OSGi");
        message.setText("This is a great mail from us.");
        mailerService.sendMailMessage(from, message);
        return "yes, mail send";
    }

    public void run() {
        consumeIt();
    }

    public void stop() {
        // nothing to do
    }
}

The run method is used to let spring start the service right after initialization of the context. So any time this bundle is loaded, it sends a mail.

But it is not working as expected. There are ClassNotFoundExceptions thrown on OSGi platform start. The packages that are used by other bundles must be imported into the consumer bundle. This may become some tedious work, but in this case only 2 packages must be imported. Put this in the osgi.bnd file of the consumer bundle:

Import-Package: *, javax.mail, javax.activation

If I start OSGi now with mvn pax:provision, the mail is sent successfully.

web app

First the jetty bundles must be imported. This installs Jetty into the OSGi framework.

mvn pax:import-bundle -DgroupId=org.mortbay.jetty -DartifactId=jetty-util -Dversion=6.1.19
mvn pax:import-bundle -DgroupId=org.mortbay.jetty -DartifactId=jetty -Dversion=6.1.19
mvn pax:import-bundle -DgroupId=org.springframework.osgi -DartifactId=jetty.start.osgi -Dversion=1.0.0
mvn pax:import-bundle -DgroupId=org.springframework.osgi -DartifactId=servlet-api.osgi -Dversion=2.5-SNAPSHOT

mvn pax:import-bundle -DgroupId=org.springframework.osgi -DartifactId=spring-osgi-web-extender -Dversion=2.0.0.M1
mvn pax:import-bundle -DgroupId=org.springframework.osgi -DartifactId=spring-osgi-web -Dversion=2.0.0.M1

mvn pax:import-bundle -DgroupId=org.springframework.osgi -DartifactId=jetty.web.extender.fragment.osgi -Dversion=1.0.1
mvn pax:import-bundle -DgroupId=org.springframework.osgi -DartifactId=cglib-nodep.osgi -Dversion=2.1.3-SNAPSHOT

After this, if you start up the OSGi platform with pax:provision, the jetty server is started. If you go to localhost:8080 there will be a HTTP 404 error, indicating that jetty is active but has no content to serve.

Now an OSGi web app bundle needs to be created…

creating web bundle project:

mvn pax:create-bundle -Dpackage=org.eknet.osgi.mailer.web -DbundleName=mailer-web -Dspring

The -Dspring creates applicationContext skeletons and adds spring dependencies. I switched to the newest available version 3.0.0.RC2. Then remove example classes.

There are still some dependencies missing:

cd mailer-web
mvn pax:import-bundle -DgroupId=org.eknet.osgi.mailer -DartifactId=org.eknet.osgi.mailer -Dversion=0.0.2-SNAPSHOT
mvn pax:import-bundle -DgroupId=org.apache.wicket -DartifactId=wicket -Dversion=1.4.2
mvn pax:import-bundle -DgroupId=org.apache.wicket -DartifactId=wicket-extensions -Dversion=1.4.2
mvn pax:import-bundle -DgroupId=org.apache.wicket -DartifactId=wicket-spring -Dversion=1.4.2
mvn pax:import-bundle -DgroupId=org.apache.wicket -DartifactId=wicket-ioc -Dversion=1.4.2
mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-web -Dversion=3.0.0.RC2
mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-context-support -Dversion=3.0.0.RC2
mvn pax:import-bundle -DgroupId=org.springframework -DartifactId=spring-web -Dversion=3.0.0.RC2

Now the trick is to create a web.xml file that uses wickets SpringWebapplicationFactory to create the wicket web application and to use a special application context called OsgiBundleXmlWebApplicationContext.

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
            http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd version="2.4" >

    <display-name>Mailer Web Application</display-name>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
        org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext
        </param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/mailer-web-osgi.xml</param-value>
    </context-param>
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
    <filter>
       <filter-name>wicket.mailer</filter-name>
       <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
       <init-param>
         <param-name>applicationFactoryClassName</param-name>
         <param-value>org.apache.wicket.spring.SpringWebApplicationFactory</param-value>
       </init-param>
       <init-param>
           <param-name>applicationBean</param-name>
           <param-value>mailerWebapp</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>wicket.mailer</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>

The classes specified in web.xml must be available to the bundle. So add those packages to the osgi.bnd file:

#-----------------------------------------------------------------
# Use this file to add customized Bnd instructions for the bundle
#-----------------------------------------------------------------

Bundle-Classpath: ., WEB-INF/classes
Web-ContextPath: mailer
Import-Package: *, \
  org.springframework.osgi.web.context.support, \
  org.eknet.osgi.mailer, \
  org.apache.wicket.spring, \
  org.springframework.web.context

Then the spring context configuration file must be located under the WEB-INF directory this time (remove META-INF/spring directory). I created the following:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/osgi"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:beans="http://www.springframework.org/schema/beans"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/osgi
            http://www.springframework.org/schema/osgi/spring-osgi.xsd">

    <beans:bean id="mailerWebapp" class="org.eknet.osgi.mailer.web.MailerWebApplication" />
    <reference id="theMailerService" interface="org.eknet.osgi.mailer.IMailerService" />
</beans:beans>

The first bean is the wicket web application. The second is the osgi service from the mailer bundle. The wicket web application is pretty simple and implements Spring's ApplicationContextAware interface. Additionally wicket ComponentCreationListener is added, so any spring bean (and therefore any OSGi service) can be injected into wicket components using the SpringBean annotation:

MailerWebApplication.java

package org.eknet.osgi.mailer.web;

import org.apache.wicket.Page;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.spring.injection.annot.SpringComponentInjector;
import org.eknet.osgi.mailer.IMailerService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * @since 26.11.2009
 */
public class MailerWebApplication extends WebApplication implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public Class<? extends Page> getHomePage() {
        return MailerHome.class;
    }

    @Override
    protected void init() {
        addComponentInstantiationListener(new SpringComponentInjector(this));
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
}

Now the last part is the page that shows a minimalistic form to fill in some values. On submitting the form the OSGi service is asked to send this as mail.

MailerHome.java

package org.eknet.osgi.mailer.web;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.eknet.osgi.mailer.IMailerService;
import org.eknet.osgi.mailer.Sender;
import org.springframework.mail.SimpleMailMessage;

/**
 * @since 26.11.2009
 */
public class MailerHome extends WebPage {

    private String toAddress;
    private String messageText;
    private String subject;

    @SpringBean
    private IMailerService theMailerService;

    public MailerHome() {

       Form form = new Form<MailerHome>("mailerForm", new CompoundPropertyModel<MailerHome>(this)) {

            @Override
            protected void onSubmit() {
                Sender sender = new Sender("Your name", "your@address.org");
                SimpleMailMessage msg = new SimpleMailMessage();
                msg.setTo(getToAddress());
                msg.setSubject(getSubject());
                msg.setText(getMessageText());
                //IMailerService ms = getMailerService();
                theMailerService.sendMailMessage(sender, msg);
            }
        };

        form.add(new TextField("toAddress"));
        form.add(new TextField("subject"));
        form.add(new TextArea("messageText"));
        add (form);
    }

    private IMailerService getMailerService() {
        return (IMailerService) ((MailerWebApplication) getApplication())
                .getApplicationContext().getBean("theMailerService", IMailerService.class);
    }
    public String getToAddress() {
        return toAddress;
    }

    public void setToAddress(String toAddress) {
        this.toAddress = toAddress;
    }

    public String getMessageText() {
        return messageText;
    }

    public void setMessageText(String messageText) {
        this.messageText = messageText;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }
}

On problems start the osgi platform with the command mvn pax:provision -Dprofiles=log and watch the log files in runner/logs/.

Date: [2009-11-26 Do]

Created: 2015-05-25 Mo 21:35

Validate