Thursday, July 31, 2014

Spring MVC, OSGI bundles and forwarding requests

I have a post in this blog with example of using OSGI servlet bridge and embedded OSGI framework. Now it's time to extend it.
Let's play with OSGI combining it with Spring MVC application.



With stackoverflow you can find some links to samples and explanations about Sprng MVC and OSGI
http://stackoverflow.com/questions/12832697/looking-for-an-osgi-with-spring-specifically-spring-mvc-tutorial
http://stackoverflow.com/questions/12331722/osgi-spring-mvc-bundle-nightmare-java-lang-classnotfoundexception-org-springf

In this article I'd like to show how does spring MVC application may use OSGI servlet-bundles.
1. My spring MVC application is going to be the main application with a fundamental business logic and it's going to be a bridge for bundles
2. I'd like to have a special bundle with a logic that is common for osgi bundles installed. Some kind of super bundle with general functionality.
3. Other bundles have it's logic implemented inside and also could interact with general bundle and main spring application forwarding its queries.

Step-by-step
create an empty web-application
mvn archetype:generate -DgroupId={project-packaging} -DartifactId={project-name} -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

create 2 folders under WEB-INF directory: one for bundles [bundles], another for ProxyServlet library [lib]  a.k.a. bridge. You may get felix http proxy jar from http://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.proxy put this jar to src/main/webapp/WEB-INF/lib/ folder. No custom bundles are available for us for a while. However we must add special bundles org.apache.felix.http.bridge and org.osgi.compendium to make our bridge work.

create spring application context file, I'd like to have it in src/main/resources/META-INF/spring. Basically, in this example we only need this context file to handle requests with corresponding controllers. The most important string here is:
...xmlns:context="http://www.springframework.org/schema/context"...
<context:component-scan base-package="com.vbashur.sample.controller" />

add some controller to com.vbashur.sample.controller

@Controller
public class SimpleController {

 @RequestMapping("/hi")
 public String sayHi() {
  return "myview";
 }
}

This controller is looking for a WEB-INF/views/myview.jsp file, let's add it
<html>
<body>
<h2>Hi there!</h2>
</body>
</html>

create OSGI framework initializers:
StartupListener - runs on servlet initialization and launches FrameworkService
FrameworkService - reads framework config, launches Felix framework (OSGI framework implementation)
ProvisionActivator - installs bundles from 'bundles' folder on demand of OSGI framework. In our case it happens when the Felix is started.
See this post http://vbashur.blogspot.kr/2014/07/osgi-servlet-bridge-sample.html#more for details of implementation of those classes.

add configuration file (framework.properties) for OSGI framework under WEB-INF folder

org.osgi.framework.storage.clean = onFirstInit

org.osgi.framework.system.packages.extra = javax.servlet;org.json;org.xml.sax;javax.xml.parsers;javax.servlet.http;version=3.1.0;\

javax.servlet.descriptor;org.apache.log4jorg.apache.felix.http.debug = true

add maven dependencies for spring, java servlet and osgi of course

<dependency>
 <groupId>org.apache.felix</groupId>
 <artifactId>org.apache.felix.framework</artifactId>
 <version>4.4.0</version>
</dependency>

<dependency>
 <groupId>org.apache.felix</groupId>
 <artifactId>org.apache.felix.main</artifactId>
 <version>4.4.0</version>
</dependency>

Finally, configure web.xml

<context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>     
  classpath*:/META-INF/spring/applicationContext*.xml 
 </param-value>
</context-param>

<listener>
 <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
 
<listener>
 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<listener>
    <listener-class>com.vbashur.sample.module.StartupListener</listener-class>
</listener>
    
<listener>
    <listener-class>org.apache.felix.http.proxy.ProxyListener</listener-class>
</listener>

<servlet>
    <servlet-name>module</servlet-name>
    <servlet-class>org.apache.felix.http.proxy.ProxyServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet> 

<servlet>
 <servlet-name>dispatcher</servlet-name>
 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
 <init-param>
  <param-name>contextConfigLocation</param-name>
  <param-value></param-value>
 </init-param>
 <load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
 <servlet-name>dispatcher</servlet-name>
 <url-pattern>/</url-pattern>
</servlet-mapping>

<servlet-mapping>
 <servlet-name>module</servlet-name>
 <url-pattern>/module/*</url-pattern>
</servlet-mapping>

Now we have a Spring MVC application which is going to be a bridge for other bundles. We need a manager module which is going to be a bundle itself and share some functionality with other bundles.

simple servlet which is available from every bundle

public class CommonServlet extends HttpServlet {

 @Override
 public void init() throws ServletException {  
 }
 
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  doPost(request, response);
 }
 
 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  response.setContentType("text/html;charset=utf-8");  
  PrintWriter out = response.getWriter();  
  out.println("Manager servlet is available from every bundle");
 }
}

for the sake of showing an exmaple I'd like to add some functionality to the manager bundle which is going to be shared between other bundles, for example I may add some abstract class which extends HttpServlet and which will be extended by other bundles.

public abstract class AbstractModuleServlet extends HttpServlet {

 public String doGreetings() {
  return "My name is " + getName();
 } 
 public abstract String getName();
}

important part of implementing bundle is its activation, we need bundle activator and service tracker

public class Activator  implements BundleActivator {

 private static BundleContext context; 
 private ServiceTracker httpServiceTracker; 

 static BundleContext getContext() {
  return context;
 }
 
 public void start(BundleContext bundleContext) throws Exception {
  Activator.context = bundleContext;   
  httpServiceTracker = new HttpServiceTracker(context);
  httpServiceTracker.open();  
 }

 public void stop(BundleContext bundleContext) throws Exception {
  Activator.context = null;
 } 
}


public class HttpServiceTracker extends ServiceTracker {
 
 private BundleContext context;  
 
 public HttpServiceTracker(BundleContext context) {
  super(context, HttpService.class.getName(), null);
  this.context = context;
 }
 
 public Object addingService(ServiceReference reference) {
  HttpService httpService = (HttpService)context.getService(reference);   
     try {
   httpService.registerServlet("/common", new CommonServlet(), null, null);
  } catch (ServletException e) {
   // TODO some error handling
   e.printStackTrace();
  } catch (NamespaceException e) {
   // TODO some error handling
   e.printStackTrace();
  }   
  return httpService;
 }
 
 @Override
 public void removedService(ServiceReference reference, Object service) {
  super.removedService(reference, service);  
  HttpService httpService = (HttpService)reference;
  httpService.unregister("/common");
 }
}
don't forget about dependencies in pom.xml

<dependency>
 <groupId>javax.servlet</groupId>
 <artifactId>javax.servlet-api</artifactId>
 <version>${servlet.version}</version>
 <scope>provided</scope>
</dependency>
 <dependency>
 <groupId>org.apache.felix</groupId>
 <artifactId>org.osgi.compendium</artifactId>
 <version>1.4.0</version>
</dependency>
<dependency>
 <groupId>org.apache.felix</groupId>
 <artifactId>org.apache.felix.http.api</artifactId>
 <version>2.3.0</version>
</dependency>

pom.xml is also responsibe for bundle build. We need the following strings in the file

...
<packaging>bundle</packaging>
...
<plugin>
 <groupId>org.apache.felix</groupId>
 <artifactId>maven-bundle-plugin</artifactId>
 <extensions>true</extensions>
 <configuration>
  <instructions>
   <Bundle-Activator>
    com.vbashur.samplemanager.Activator
   </Bundle-Activator>
   <Import-Package>
    *;resolution:=optional;
   </Import-Package>
   <Export-Package>
    com.vbashur.samplemanager.module
   </Export-Package>
  </instructions>
 </configuration>
</plugin>

In order to check whether everything is correct in your bundle run 'mvn clean install' if it's been built successfully you find the bundle jar in a target directory of samplemanager project. Copy this jar to the default bundle location of samplebridge project and run your application on server. Check if there some errors on startup. if not and you can query your manager servlet with <protocol>://<host_ip>:<port>/samplebridge/module/common [by default http://localhost:8080/samplebridge/module/common] it means that everything is OK your bridge works fine!


Next step is implementation of servlet bundles which will use manager bundle.
Module A

public class ModuleServletA extends AbstractModuleServlet {

 @Override
 public String getName() {
  return "Module A";
 }
 
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  doPost(request, response);
 }
 
 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  response.setContentType("text/html;charset=utf-8");  
  PrintWriter out = response.getWriter();  
  out.println(doGreetings());
 }
}

Activator remains the same with manager module, service tracker has some minor changes with servlet alias and responsible servlet

public class HttpServiceTracker extends ServiceTracker {
 
 private BundleContext context;  
 
 public HttpServiceTracker(BundleContext context) {
  super(context, HttpService.class.getName(), null);
  this.context = context;
 }
 
 public Object addingService(ServiceReference reference) {
  HttpService httpService = (HttpService)context.getService(reference);   
     try {
   httpService.registerServlet("/a", new ModuleServletA(), null, null);
  } catch (ServletException e) {
   // TODO some error handling
   e.printStackTrace();
  } catch (NamespaceException e) {
   // TODO some error handling
   e.printStackTrace();
  }   
  return httpService;
 }
 
 @Override
 public void removedService(ServiceReference reference, Object service) {
  super.removedService(reference, service);  
  HttpService httpService = (HttpService)reference;
  httpService.unregister("/a");
 }
}

pom.xml is almost identical with pom.xml of the servletmanager project, it's also required the dependency

<dependency>
 <groupId>com.vbashur</groupId>
 <artifactId>samplemanager</artifactId>
 <version>0.0.1-SNAPSHOT</version>
</dependency>

Repeat all the build procedures like for manager module. By default the module servlet should be available by link: http://localhost:8080/samplebridge/module/a

In Module B I'd like to add several special servlets
- ModuleServletIncludeA must use 'include' method processing queries to module A
- ModuleServletForwardToSpring forwards queries to the spring MVC application (found out that forwarding doesn't work in this case, so 'include' is using)
- ModuleServletForwardToCommon forwards queries to manager module


public class ModuleServletIncludeA  extends AbstractModuleServlet {

 @Override
 public String getName() {
  return "Module which includes to A";
 }
 
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  doPost(request, response);
 }
 
 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  response.setContentType("text/html;charset=utf-8");  
  PrintWriter out = response.getWriter();  
  out.println(doGreetings());
  RequestDispatcher rd = request.getRequestDispatcher("/a");
  rd.include(request, response);   
 }
}

public class ModuleServletForwardToSpring extends AbstractModuleServlet {

 @Override
 public String getName() {
  return "Module which forwards to Spring MVC";
 }

 protected void doGet(HttpServletRequest request,
   HttpServletResponse response) throws ServletException, IOException {
  doPost(request, response);
 }

 protected void doPost(HttpServletRequest request,
   HttpServletResponse response) throws ServletException, IOException {
  response.setContentType("text/html;charset=utf-8");
  PrintWriter out = response.getWriter();
  out.println(doGreetings());
  ServletContext sc = getServletContext();
  sc = sc.getContext("/samplebridge");

  RequestDispatcher rd = sc.getRequestDispatcher("/hi");
//  rd.forward(request, response); // won't work
  rd.include(request, response);
 }
}


public class ModuleServletForwardToCommon extends AbstractModuleServlet {

 @Override
 public String getName() {
  return "Module which forwards to Commmon";
 }
 
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  doPost(request, response);
 }
 
 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  response.setContentType("text/html;charset=utf-8");  
  PrintWriter out = response.getWriter();  
  out.println(doGreetings());
  RequestDispatcher rd = request.getRequestDispatcher("/common");
  rd.forward(request, response);   
 }
}

So we got a few projects where servlet bundles can exchenage its requests with the main bridge project and manager module which contain some common logic. With OSGI services you may get more powerful application where bundle context shares some common object for all installed bundles and every bundle may use the functionality of this object. That's a really nice stuff of OSGI [See a good sample about OSGI services http://baptiste-wicht.com/posts/2010/07/osgi-hello-world-services.html]

One rotten apple spoils the bunch...
- for request forwarding we only may use request.getRequestDispatcher("/somepath"), but not getServletContext.getRequestDispatcher("/somepath") which causes error. Seems like a bug in Filex bridge implementation [https://issues.apache.org/jira/browse/FELIX-4589]
- 'forward' method doesn't work the Sprng MVC context request dispatcher: see ModuleServletForwardToSpring servlet

Source code is available here: https://bitbucket.org/vbashur/diff/src (projects which start from sample*)

3 comments:

  1. Does this work with Spring 4.1.x ?

    ReplyDelete
  2. Kalyani springs always believe in supplying the best quality product at a reasonable price to their customers. Our main motive is customer's satisfaction. The appreciation of our customers will lead the company to become one of the reputed names in industrial spring manufacturer, exporter, and supplier.
    spring manufacturing process

    ReplyDelete