Sunday, December 18, 2016

Angular2 + Webpack + Maven + SpringMVC + Swagger

This post is to track the steps followed to integrate an Angular2 web application with a Maven Spring MVC application, that uses Swagger annotations to annotate the Model and API objects and generate angular2-typescript code.

Glossary


Term Description
Angular2 Angular2 is a javascript framework to develop web applications for desktop and mobile. More about Angular2 here.
Webpack Webpack is a module bundler for javascript applications. More about Webpack here.
Maven Maven is a dependency management and build tool. More about Maven here.
Spring MVC Spring MVC provides a framework to dispatch HTTP requests to request handlers in a highly-configurable, extensible manner. More about Spring MVC here.
Swagger Swagger is an API framework to document and consume RESTful APIs. More about Swagger here.

Step-by-step Guide

Setup Angular2 Webapp

In a maven project, create the directory where angular2 code will be present:
 
# For example, in my-service
$ cd my-service/src/main
$ mkdir my-webapp
$ cd my-webapp

Create package.json and add the following configuration (Note: Replace all the ${webapp.*} placeholders in the file.):
 

{
  "name": "${webapp.name}",
  "version": "${webapp.version}",
  "description": "${webapp.description}",
  "readme": "${webapp.readme.url}",
  "repository": {
    "type": "git",
    "url": "${webapp.repository.url}"
  },
  "scripts": {
    "start": "webpack-dev-server --inline --progress --port 8080",
    "build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
  },
  "license": "ISC",
  "dependencies": {
    "@angular/common": "~2.2.0",
    "@angular/compiler": "~2.2.0",
    "@angular/core": "~2.2.0",
    "@angular/forms": "~2.2.0",
    "@angular/http": "~2.2.0",
    "@angular/platform-browser": "~2.2.0",
    "@angular/platform-browser-dynamic": "~2.2.0",
    "@angular/platform-server": "~2.2.0",
    "@angular/router": "~3.2.0",
    "assets-webpack-plugin": "^3.4.0",
    "bootstrap": "^3.3.6",
    "core-js": "^2.4.1",
    "font-awesome": "^4.6.3",
    "moment": "^2.14.1",
    "ng2-bootstrap": "1.1.16",
    "ng2-modal": "0.0.22",
    "rxjs": "5.0.0-beta.12",
    "zone.js": "^0.6.25"
  },
  "devDependencies": {
    "@types/core-js": "^0.9.35",
    "@types/jasmine": "^2.5.35",
    "@types/node": "^6.0.45",
    "angular2-template-loader": "^0.4.0",
    "awesome-typescript-loader": "^3.0.0-beta.13",
    "connect-history-api-fallback": "^1.2.0",
    "copy-webpack-plugin": "^4.0.0",
    "css-loader": "^0.23.0",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.8.5",
    "html-loader": "^0.4.3",
    "html-webpack-plugin": "^2.24.1",
    "http-proxy-middleware": "^0.17.0",
    "jasmine-core": "^2.4.1",
    "null-loader": "^0.1.1",
    "phantomjs-prebuilt": "^2.1.7",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.5.4",
    "script-ext-html-webpack-plugin": "^1.3.2",
    "style-loader": "^0.13.1",
    "to-string-loader": "^1.1.4",
    "ts-helpers": "1.1.2",
    "ts-node": "^1.7.0",
    "typescript": "^2.1.4",
    "webpack": "^1.13.3",
    "webpack-dev-middleware": "^1.6.1",
    "webpack-dev-server": "^1.16.2",
    "webpack-md5-hash": "^0.0.5",
    "webpack-merge": "^1.0.2"
  }
}

Create tsconfig.json and add the following configuration:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}

Create index.html, the landing page for the webapp.

<!DOCTYPE html>
<html>
<head>
    <title>My UI Application</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="/static/ext/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <my-app>Loading My UI Application ...</my-app>
    <script src="/static/ext/bootstrap/js/jquery.js"></script>
    <script src="/static/ext/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

Create .npmrc file with the following contents. This is needed while compiling the angular project.

strict-ssl=false
registry=http://registry.npmjs.org/

Add the app folder that actually contains all the modules, components, services corresponding to the webapp. Create client, components, service sub folders within the app folder. Create app.module.ts file in the app folder, with the following content:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { Ng2BootstrapModule } from 'ng2-bootstrap/ng2-bootstrap';
import { RouterModule, PreloadAllModules } from '@angular/router';
import {APP_BASE_HREF, Location} from '@angular/common';
import { AppComponent } from './app.component';
 
 
@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        Ng2BootstrapModule,
        RouterModule.forRoot([
        ])
    ],
    providers: [
        {
            provide: APP_BASE_HREF,
            useValue: window['_app_base'] || '/'
        }
    ]
})
export class AppModule {
}

Create app.component.ts with the following content:

import { Component, ViewContainerRef, OnDestroy } from '@angular/core';
@Component({
    selector: 'my-app'
})
export class AppComponent {
}

Distribute the dependencies into two different typescript files
  • polyfills.ts: this will be loaded before the tag in index.html.
  • vendor.ts: this will be loaded before the tag in index.html.

//polyfills.ts

import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');
if (process.env.ENV === 'production') {
  // Production
} else {
  // Development
  Error['stackTraceLimit'] = Infinity;
  require('zone.js/dist/long-stack-trace-zone');
}


//vendor.ts

// Angular
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
// RxJS
import 'rxjs';
// Other vendors for example jQuery, Lodash or Bootstrap
// You can import js, ts, css, sass, ...

And main.ts:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app.module';
if (process.env.ENV === 'production') {
  enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);

Now the angular2 webapp directory will be as follows:

my-service
|- src
   |- main
      |- my-webapp
         |- app
            |- client -- This folder contains the swagger REST client that can be used to talk to the backend service.
               |- model
                  |- 
            |- components
               |- 
            |- service
               |- 
            |- app.module.ts -- Main module.
            |- app.component.ts -- Top level component that will load the rest of the components.
            |- main.ts -- entry point.
            |- polyfills.ts
            |- vendor.ts
         |- .npmrc
         |- index.html
         |- package.json
         |- tsconfig.json

Configure Webpack

The package.json file created above already has the webpack dependencies included, and the scripts already are configured to run webpack command. Now, create webpack.config.js in the webapp root folder, i.e. in my-webapp.

module.exports = require('./config/webpack.dev.js');

Create config directory under my-webapp. Add the following files:

helpers.js


var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
  args = Array.prototype.slice.call(arguments, 0);
  return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

webpack.common.js


var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');
const { CheckerPlugin } = require('awesome-typescript-loader')
module.exports = {
  entry: {
    'app': './app/main.ts',
    'polyfills': './app/polyfills.ts',
    'vendor': './app/vendor.ts'
  },
  resolve: {
    extensions: ['', '.ts', '.js']
  },
  module: {
    loaders: [
      {
        test: /\.ts$/,
        loaders: ['awesome-typescript-loader', 'angular2-template-loader']
      },
      {
        test: /\.html$/,
        loader: 'html'
      },
      {
        test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
        loader: 'file?name=assets/[name].[hash].[ext]'
      },
      {
        test: /\.css$/,
        exclude: helpers.root('app'),
        loader: ExtractTextPlugin.extract('style', 'css?sourceMap')
      },
      {
        test: /\.css$/,
        include: helpers.root('app'),
        loader: 'raw'
      }
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: ['app', 'polyfills', 'vendor']
    }),
    new HtmlWebpackPlugin({
      template: './index.html',
      baseUrl: '/'
    }),
    new CheckerPlugin()
]
};

webpack.dev.js


var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
module.exports = webpackMerge(commonConfig, {
  devtool: 'cheap-module-eval-source-map',
  output: {
    path: helpers.root('dist'),
    publicPath: 'http://localhost:8080/',
    filename: '[name].js',
    chunkFilename: '[id].chunk.js'
  },
  plugins: [
    new ExtractTextPlugin('[name].css')
  ],
  devServer: {
    historyApiFallback: {
      index: "/",
      baseUrl: "/"
    },
    stats: 'minimal',
    proxy: [
      {
        context: [
          
        ],
        target: '',
        secure: false
      }
    ]
  }
});

webpack.prod.js


var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
module.exports = webpackMerge(commonConfig, {
  devtool: 'source-map',
  output: {
    path: helpers.root('dist'),
    publicPath: '/assets/',
    filename: '[name].[hash].js',
    chunkFilename: '[id].[hash].chunk.js'
  },
  htmlLoader: {
    minimize: false // workaround for ng2
  },
  plugins: [
    new webpack.NoErrorsPlugin(),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({ // https://github.com/angular/angular/issues/10618
      mangle: {
        keep_fnames: true
      }
    }),
    new ExtractTextPlugin('[name].[hash].css'),
    new webpack.DefinePlugin({
      'process.env': {
        'ENV': JSON.stringify(ENV)
      }
    })
  ]
});

At the end of this, the my-webapp folder would be as follows:

my-service
|- src
   |- main
      |- my-webapp
         |- app
            |- client -- This folder contains the swagger REST client that can be used to talk to the backend service.
               |- model
                  |- 
            |- components
               |- 
            |- service
               |- 
            |- app.module.ts -- Main module.
            |- app.component.ts -- Top level component that will load the rest of the components.
            |- main.ts -- entry point.
            |- polyfills.ts
            |- vendor.ts
         |- config
            |- helpers.js
            |- webpack.common.js
            |- webpack.dev.js
            |- webpack.prod.js
         |- .npmrc
         |- index.html
         |- package.json
         |- tsconfig.json

Now, the set up is ready to bring up the dev instance of the angular app with the following commands:


# Run npm install, to install all the dependencies under the node_modules directory.
$ npm install
 
 
# A sample output would be as follows:
 
+-- @angular/common@2.2.4
+-- @angular/compiler@2.2.4
+-- @angular/core@2.2.4
+-- @angular/forms@2.2.4
+-- @angular/http@2.2.4
+-- @angular/platform-browser@2.2.4
+-- @angular/platform-browser-dynamic@2.2.4
+-- @angular/platform-server@2.2.4
| `-- parse5@2.2.3
+-- @angular/router@3.2.4
+-- @types/core-js@0.9.35
+-- @types/jasmine@2.5.38
+-- @types/node@6.0.51
+-- angular2-template-loader@0.4.0
| `-- loader-utils@0.2.16
|   +-- big.js@3.1.3
|   +-- emojis-list@2.1.0
|   `-- json5@0.5.1
+-- assets-webpack-plugin@3.5.0
.....
.....
.....
.....
.....
+-- webpack-md5-hash@0.0.5
| `-- md5@2.2.1
|   +-- charenc@0.0.1
|   +-- crypt@0.0.1
|   `-- is-buffer@1.1.4
+-- webpack-merge@1.1.1
| +-- lodash.clonedeep@4.5.0
| +-- lodash.differencewith@4.5.0
| +-- lodash.isequal@4.4.0
| +-- lodash.isfunction@3.0.8
| +-- lodash.isplainobject@4.0.6
| +-- lodash.mergewith@4.6.0
| `-- lodash.unionwith@4.6.0
`-- zone.js@0.6.26
npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.0.15
$
 
# Run npm start to start the webpack's dev server
$ npm start
 
# A sample output would be as follows:
[at-loader] Checking started in a separate process...
[at-loader] Ok, 0.874 sec.                                                                                                                           chunk    {0} app.js (app) 959 kB {1} [rendered]
chunk    {1} polyfills.js (polyfills) 246 kB {2} [rendered]
chunk    {2} vendor.js (vendor) 2.6 MB [rendered]
Child html-webpack-plugin for "index.html":
    chunk    {0} index.html 542 bytes [rendered]
webpack: bundle is now VALID.
 
 
 
# Run npm run build to generate the artifacts that will be used when the angular webapp is hosted from the backend server
$ npm run build
 
 < some output here...>

Upon npm run build, dist directory will be created with the following files:

my-service
|- src
   |- main
      |- my-webapp
         |- dist
            |- app.*.js
            |- app.*.js.map
            |- index.html
            |- pollyfills.*.js
            |- pollyfills.*.js.map
            |- vendor.*.js
            |- vendor.*.js.map

Spring MVC Configuration

As part of Spring MVC configuration, we will add the following:
  1. Mapping /assets/* to the dist folder created above, that gets copied to WEB-INF/my-webapp/dist, more about that in the pom.xml section below.
  2. Exclude /assets/* from Authentication.

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**" />
        <mvc:exclude-mapping path="/assets/**" />
    </mvc:interceptor>
</mvc:interceptors>
<mvc:resources mapping="/assets/**" location="/WEB-INF/my-webapp/dist/" />

In the Spring MVC controller, load the index.html file for all the paths starting with "/ui", assuming that the Angular2 Webapp will be rendered at this path. If you would like to render the webapp at the root, then the controller should load index.html for "", "/", "/**".

// If the angular2 webapp should be rendered in the root context.
@RequestMapping(value = { "", "/**" }, method = RequestMethod.GET)
// If the angular2 webapp should be rendered in the /ui context.
@RequestMapping(value = {"/ui", "/ui/**" }, method = RequestMethod.GET)
@ResponseBody
public String loadAngular2IndexHtml() throws IOException
{
    return ;
}

Maven Build Configuration - pom.xml

As part of pom.xml, we would like to
  1. Compile and build the angular2 webapp using webpack
  2. Copy the angular2 webapp artifacts generated by webpack's npm run build method, to WEB-INF directory, during the processing of preparing the war file.
  3. Clean up the angular2 build artifacts as part of project clean up step.
  4. (Optional) Generate the swagger client module for angular2 webapp.

<properties>
    <nodeVersion>v6.1.0</nodeVersion>
    <npmVersion>3.8.6</npmVersion>
</properties>
 
<build>
    <plugins>
        <!-- Step 1: Compile and build the angular2 webapp -->
        <plugin>
            <groupId>com.github.eirslett</groupId>
            <artifactId>frontend-maven-plugin</artifactId>
            <version>1.0</version>
            <configuration>
                <workingDirectory>src/main/my-webapp/</workingDirectory>
                <installDirectory>target/temp</installDirectory>
            </configuration>
            <executions>
                <!-- It will install nodejs and npm -->
                <execution>
                    <id>install node and npm</id>
                    <goals>
                        <goal>install-node-and-npm</goal>
                    </goals>
                    <configuration>
                        <workingDirectory>src/main/my-webapp/</workingDirectory>
                        <installDirectory>target/temp</installDirectory>
                        <nodeVersion>${nodeVersion}</nodeVersion>
                        <npmVersion>${npmVersion}</npmVersion>
                    </configuration>
                </execution>
                <!-- It will execute command "npm install" inside "/angular" directory -->
                <execution>
                    <id>npm install</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <configuration>
                        <workingDirectory>src/main/my-webapp/</workingDirectory>
                        <installDirectory>target/temp</installDirectory>
                        <arguments>install</arguments>
                    </configuration>
                </execution>
                <execution>
                    <id>npm build</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <configuration>
                        <workingDirectory>src/main/my-webapp/</workingDirectory>
                        <installDirectory>target/temp</installDirectory>
                        <arguments>run build</arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
 
 
        <!-- Step 2: Copy the angular2 build artifacts to the war generated for the backend webapp -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>2.6</version>
            <configuration>
                <webResources>
                    <resource>
                        <directory>src/main/my-webapp/</directory>
                        <targetPath>WEB-INF/my-webapp</targetPath>
                        <excludes>
                            <exclude>**/node_modules/**</exclude>
                            <exclude>**/node/**</exclude>
                            <exclude>**/typings/**</exclude>
                        </excludes>
                    </resource>
                </webResources>
            </configuration>
        </plugin>
 
 
        <!-- Step 3: Clean up the build artifacts -->
        <plugin>
            <artifactId>maven-clean-plugin</artifactId>
            <version>3.0.0</version>
            <configuration>
                <filesets>
                    <fileset>
                        <directory>src/main/my-webapp/dist</directory>
                        <includes>
                            <include>**/*</include>
                        </includes>
                        <followSymlinks>false</followSymlinks>
                    </fileset>
                    <fileset>
                        <directory>src/main/my-webapp/node_modules</directory>
                        <includes>
                            <include>**/*</include>
                        </includes>
                        <followSymlinks>false</followSymlinks>
                    </fileset>
                    <fileset>
                        <directory>src/main/my-webapp/node</directory>
                        <includes>
                            <include>**/*</include>
                        </includes>
                        <followSymlinks>false</followSymlinks>
                    </fileset>
                </filesets>
            </configuration>
        </plugin>
 
        <!-- Step 4: (Optional) Generate the Angular2-TypeScript client module for the backend REST services annotated with Swagger annotations.-->
        <plugin>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-codegen-maven-plugin</artifactId>
            <version>2.2.1</version>
            <executions>
                <execution>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                    <configuration>
                        <inputSpec>${project.basedir}/src/main/webapp/my-service.swagger.json</inputSpec>
                        <language>typescript-angular2</language>
                        <output>src/main/my-webapp/app/client/</output>
                        <configOptions>
                            <supportsES6>true</supportsES6>
                        </configOptions>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>


Tuesday, October 27, 2015

Using SpringFox for API document generation.

While working on Spring MVC based RESTful services that expose APIs for other developers to use, I needed a library that could read the API specifications in the Controller classes, generate API documentation (so that it is always up-to-date with the changes to the API spec) and better if it can provide a way to invoke these APIs. Springfox's implementation of Swagger appeared to fit these requirements perfectly.

This post is a log of changes I made to my Spring MVC RESTful service to integrate with Springfox's Swagger implementation.
  1. Included the maven dependencies for swagger-springfox to expose the API details and swagger-springfox-ui to render the documentation and provide a "Try it out" interface to invoke these APIs.
  2.  
        <dependency>
            <groupid>io.springfox</groupid>
            <artifactid>springfox-swagger2</artifactid>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupid>io.springfox</groupid>
            <artifactid>springfox-swagger-ui</artifactid>
            <version>2.2.2</version>
        </dependency>
    
    
  3. Created a Configuration class to initialize SpringFox
  4. import static com.google.common.collect.Lists.newArrayList;
    import static springfox.documentation.schema.AlternateTypeRules.newRule;
    
    import org.joda.time.LocalDate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.context.request.async.DeferredResult;
    
    import com.fasterxml.classmate.TypeResolver;
    
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.builders.ResponseMessageBuilder;
    import springfox.documentation.schema.ModelRef;
    import springfox.documentation.schema.WildcardType;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2
    public class SwaggerConfig
    {
        @Autowired
        private TypeResolver typeResolver;
    
        @Bean
        public Docket petApi()
        {
            return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build()
                .pathMapping("/")
                .directModelSubstitute(LocalDate.class, String.class)
                .genericModelSubstitutes(ResponseEntity.class)
                .alternateTypeRules(
                    newRule(
                        typeResolver.resolve(
                            DeferredResult.class,
                            typeResolver.resolve(ResponseEntity.class, WildcardType.class)),
                        typeResolver.resolve(WildcardType.class)))
                .useDefaultResponseMessages(false)
                .globalResponseMessage(
                    RequestMethod.GET,
                    newArrayList(
                        new ResponseMessageBuilder()
                            .code(500)
                            .message("500 message")
                            .responseModel(new ModelRef("Error"))
                            .build()))
                .enableUrlTemplating(false);
        }
    
    }
    
    
  5. With the addition of Springfox Configuration above, the test cases started failing due to missing ServletContext bean. Added the @WebAppConfiguration annotation to the parent test class that also has the test spring configuration initialized.
  6. 
    
    import org.junit.Ignore;
    import org.junit.runner.RunWith;
    import org.springframework.test.annotation.DirtiesContext;
    import org.springframework.test.annotation.DirtiesContext.ClassMode;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    import org.springframework.test.context.web.WebAppConfiguration;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = { "classpath:test-application-spring-config.xml" })
    @DirtiesContext(classMode = ClassMode.AFTER_CLASS)
    @WebAppConfiguration
    @Ignore("Ingoring an abstract test class")
    public abstract class AbstractIntegrationTest
    {
    
    }
    
    
    
  7. Added the following resource mappings to serve the swagger related resources from springfox-swagger-ui.jar.
  8. 
    <mvc:resources location="classpath:/META-INF/resources/swagger-ui.html" mapping="swagger-ui.html">    <mvc:resources location="classpath:/META-INF/resources/webjars/" mapping="/webjars/**">
    </mvc:resources></mvc:resources>
    
    
Notes:
  1. Initially, I set the "enableUrlTemplating" attribute to "true" in SwaggerConfiguration. Due to this the URLs in "Try it out" section included placeholders for the request parameters, making this feature use less.

    E.g.
    URL when the attribute is set to true:
            http://localhost:8080/my-api{?param1,param2}?param1=&param2=&

    URL after the attribute value is changed to false:
            http://localhost:8080/my-api?param1=&param2=&
  2. At first, I included the mvc resource mappings in the SwaggerConfiguration class itself, by extending the WebMvcConfigurerAdapter class, but access to swagger-ui.html returned 404 error. So, I removed this from here and added them to the XML configuration and then the swagger-ui.html came up properly.
  3.     @Override
        public void addResourceHandlers(final ResourceHandlerRegistry registry)
        {
            registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
    
    
  4. I tried to render swagger-ui.html under a different URL (e.g. /my-service-api) but, due to hard coded lookup for swagger-ui.html in the springfox.js, even though the page loaded, it couldn't proceed further to make call to the /v2/api-docs to get the JSON and render it.

Sunday, December 21, 2014

Configuring Log4J with Slf4J in Spring-based application

While working with a Spring Boot based application, the spring-boot jar brings in the sl4j related jars which seems to be configured to jakarta-commons-logging by default. So, the log4j.properties in the classpath was never read by the log4j system.

To make log4j work with sl4j in this scenario, the sl4j dependencies coming from spring-boot have been excluded as follows:


<dependency>
 <groupid>org.springframework.boot</groupid>
 <artifactid>spring-boot-starter-data-rest</artifactid>
 <version>${spring-boot-version}</version>
 <exclusions>
  <exclusion>
   <groupid>org.slf4j</groupid>
   <artifactid>slf4j-api</artifactid>
  </exclusion>
  <exclusion>
   <groupid>org.slf4j</groupid>
   <artifactid>jul-to-slf4j</artifactid>
  </exclusion>
  <exclusion>
   <groupid>org.slf4j</groupid>
   <artifactid>log4j-over-slf4j</artifactid>
  </exclusion>
  <exclusion>
   <groupid>org.slf4j</groupid>
   <artifactid>jcl-over-slf4j</artifactid>
  </exclusion>
  <exclusion>
   <groupid>ch.qos.logback</groupid>
   <artifactid>logback-classic</artifactid>
  </exclusion>
 </exclusions>
</dependency>

Then, the sl4j and log4j dependencies are pulled in separately, as follows:

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
            <scope>runtime</scope>
        </dependency>

The Spring Test library seems to require JCL (jakarta-commons-logging) as it was throwing the following exception:


java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.clinit(SpringJUnit4ClassRunner.java:86)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
 at java.lang.reflect.Constructor.newInstance(Unknown Source)


Included the jcl-over-slf4j dependency to fix the above exception:
 
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>${slf4j.version}</version>
</dependency>


Now, the log4j.properties file in src/main/resources is read by the log4j system, without any additional configuration.

Tuesday, January 15, 2013

Functional testing in SOA environment

My first project in a service oriented environment has reached functional testing stage. The product involves processing large amounts of business objects whose reference data is obtained from other subsystems through service calls, in production.

While devising the strategy for functional testing, the issue was how to get this reference data in a test environment.

Unit Testing

During unit testing, the service calls were mocked using a mocking library (Mockito in our case), and the expected responses were returned using when - thenReturn construct, as follows:

 

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.mockito.MockitoAnnotations;
import org.mockito.Mock;
import org.junit.Before;
import org.junit.Test;

public class SweepTest 
{
    /*
     * Specify that the service wrapper should be mocked.
     */
    @Mock
    private XServiceWrapper xServiceWrapper;

    /*
     * SomeBusinessObjectManager has reference to XServiceWrapper and we want the 
     * mocked instance to be injected instead of the normal implementation.
     */
    @InjectMocks
    @Autowired
    private SomeBusinessObjectManager boManager;

    @Before
    public void setup()
    {
         MockitoAnnotations.initMocks(this);

         /*
          * Mock the response object from the service.
          */
         XServiceResponse serviceResponse = mock(XServiceResponse.class);
         when(serviceResponse.getXValue()).thenReturn(myValue);

         /*
          * Mock the service call and return mocked response object.
          */
         when(xServiceWrapper.serviceCall()).thenReturn(serviceResponse);
    }
}



Functional Testing

During functional testing, we identified golden set of use cases that necessarily and sufficiently cover the behavior of the system in various scenarios. The data for various internal objects for these golden set of use cases is prepared. But, it is not feasible to get relevant data from the external services that would suit the input data for a given use case. So, the following changes were done to the project w.r.t interacting with external services:
  1. Define an ExternalServiceFacade interface that would define all the calls to be made to different external services.
  2. 
    /**
     * ExternalServiceFacade.java
     * 
     * $Source$
     */
    /*
     * Copyright (c) 2013 Krovi.com, Inc. All rights reserved.
     */
    
    package com.krovi.app.manager;
    
    import com.krovi.services.BusinessObjectA;
    import com.krovi.services.BusinessObjectB;
    import com.krovi.services.BusinessObjectC;
    
    /**
     * This class follows the Facade pattern and provides APIs to
     * interact with various external services that provide reference data for the given object IDs.
     * 
     * @author krovi
     * @version $Id$
     */
    public interface ExternalServiceFacade
    {
        BusinessObjectA lookupBusinessObjectA(String referenceId);
    
        BusinessObjectB lookupBusinessObjectB(String referenceId1, String referenceId2);
    
        BusinessObjectC lookupBusinessObjectC(String referenceId1, String referenceId2, Long referenceValue3);
    
    }
    
    
    
  3. Provide an implementation that delegates the calls to the actual services and obtains the responses. This implementation will be used in production scenarios.
  4. 
    /**
     * ExternalServiceFacadeImpl.java
     * 
     * $Source$
     */
    /*
     * Copyright (c) 2013 Krovi.com, Inc. All rights reserved.
     */
    
    package com.krovi.app.manager;
    
    import com.krovi.services.BusinessObjectA;
    import com.krovi.services.BusinessObjectAService;
    import com.krovi.services.BusinessObjectAServiceClient;
    import com.krovi.services.BusinessObjectB;
    import com.krovi.services.BusinessObjectBService;
    import com.krovi.services.BusinessObjectBServiceClient;
    import com.krovi.services.BusinessObjectC;
    import com.krovi.services.BusinessObjectCService;
    import com.krovi.services.BusinessObjectCServiceClient;
    
    /**
     * @author krovi
     * @version $Id$
     */
    public class ExternalServiceFacadeImpl implements ExternalServiceFacade
    {
        @Autowired
        private BusinessObjectAService boAService;
    
        @Autowired
        private BusinessObjectBService boBService;
    
        @Autowired
        private BusinessObjectCService boCService;
    
        @Override
        public BusinessObjectA lookupBusinessObjectA(String referenceId)
        {
            BusinessObjectAServiceClient client = boAService.getClient();
            return client.lookupBusinessObjectA(referenceId);
        }
    
        @Override
        public BusinessObjectB lookupBusinessObjectB(String referenceId1, String referenceId2)
        {
            BusinessObjectBServiceClient client = boBService.getClient();
            return client.lookupBusinessObjectB(referenceId1, referenceId2);
        }
    
        @Override
        public BusinessObjectC lookupBusinessObjectC(String referenceId1, String referenceId2, Long referenceValue3)
        {
            BusinessObjectCServiceClient client = boCService.getClient();
            return client.lookupBusinessObjectC(referenceId1, referenceId2, referenceValue3);
        }
    
    }
    
    
    
  5. Provide a mock implementation that will connect to a local data store that is populated with the expected data and returns it. This implementation will be used in functional testing scenario.
  6.  
    /**
     * MockExternalServiceFacadeImpl.java
     * 
     * $Source$
     */
    /*
     * Copyright (c) 2013 Krovi.com, Inc. All rights reserved.
     */
    
    package com.krovi.app.manager;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    
    import com.krovi.services.BusinessObjectA;
    import com.krovi.services.BusinessObjectB;
    import com.krovi.services.BusinessObjectC;
    
    /**
     * @author krovi
     * @version $Id$
     */
    public class MockExternalServiceFacadeImpl implements ExternalServiceFacade
    {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        public MockExternalServiceFacadeImpl()
        {
            setupDBObjects();
        }
    
        private void setupDBObjects()
        {
            /**
             * 1. This method creates DB tables corresponding to the structures of BusinessObject* classes. 2. The test
             * harness would insert test data into these tables as per the use cases.
             */
        }
    
        @Override
        public BusinessObjectA lookupBusinessObjectA(String referenceId)
        {
            // Look up the database instead of making a service call. The database would contain the appropriate object
            // required for the test case that is being currently run.
            return null;
        }
    
        @Override
        public BusinessObjectB lookupBusinessObjectB(String referenceId1, String referenceId2)
        {
            // Look up the database instead of making a service call. The database would contain the appropriate object
            // required for the test case that is being currently run.
            return null;
        }
    
        @Override
        public BusinessObjectC lookupBusinessObjectC(String referenceId1, String referenceId2, Long referenceValue3)
        {
            // Look up the database instead of making a service call. The database would contain the appropriate object
            // required for the test case that is being currently run.
            return null;
        }
    
    }
    
    
  7. Modify all calls to services in the entire code base to go through ExternalServiceFacade.
  8. The spring configuration files for production and testing stages are modified to inject the appropriate implementation of the ExternalServiceFacade.

Wednesday, October 17, 2012

@Memoize in Java using Spring and Aspects

Recently, while writing an application in service oriented architecture, I had a couple of places in the code where I needed to make a service call to get the details by a particular application key. I was wondering about duplicate calls, i.e. multiple service calls made to get the details for the same application key. As we all know, service calls are costly with all the marshalling, unmarshalling, cache lookups, database lookups involved, I wanted to cache the results of a service call by application key or keys and this is a clear cross-cutting concern. Having programmed in Perl and Python that have the memoize option, I thought a Memoize aspect would be most appropriate here. As always, I was looking for Spring's Aspect Oriented Programming APIs and though it was tricky to get the set up right, I ended up with a very small amount of code that does the basic memoization. Since the set up was tricky, I thought I would document here for reference:


  1. Set up Spring, AspectJ, CGLib and ASM for the application.
    1. The following Spring jars are needed:
      1. spring-aop (v3.3.1)
      2. spring-aspects (v3.3.1)
      3. spring-beans (v3.3.1)
      4. spring-context (v3.3.1)
      5. spring-context-support (v3.3.1)
      6. spring-core (v3.3.1)
      7. spring-test (v3.3.1)
      8. spring-expression (v3.3.1)
      9. spring-asm (v3.3.1)
    2. The following AspectJ jars are needed for load-time-weaving and aspectj runtime.
      1. aspectj-rt (v1.6.9)
      2. aspectj-weaver (v1.5.4)
      3. aop-alliance (v1.0)
    3. The following CGLib jars are needed for code generation
      1. cglib (v2.2.3)
    4. The following ASM jars are needed for byte-code manipulation
      1. asm (v3.3.1)
    5. Commons logging jar needed by Spring
      1. commons-logging (v1.1.1)
  2. The CGLib and ASM libraries should be compatible with each other. So, if you choose a different version of any of these jars, make sure to select the compatible other one. Otherwise, you will end up with a runtime error (NoSuchMethodError).
  3. Enable aspects in your spring application using the following configuration:
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
                http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
                ">
    
        <context:component-scan base-package="com.vikdor.aspects" />
        <aop:aspectj-autoproxy proxy-target-class="true" />
    </beans>
    
    
  4. Define the annotation that is going to be used in the advice:
    
    import java.lang.annotation.Retention;
    
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Memoize
    {
    
    }
    
    
  5. Write a simple Aspect class, annotate it with @Aspect and define an @Around advice for all the methods that use the @Memoize annotation, as follows:
    
    @Aspect
    @Component
    public class MemoizeAspect
    {
        private WeakHashMap<Object[], Object> cache = new WeakHashMap<Object[], Object>();
    
        @Around("@annotation(com.vikdor.aspects.Memoize)")
        public Object handledMemoize(ProceedingJoinPoint pjp) throws Throwable
        {
            System.out.println("Handling memoize");
            Object[] args = pjp.getArgs();
    
            if (args != null)
            {
                Object response = cache.get(Arrays.asList(args));
                if (response != null)
                {
                    return response;
                }
            }
            Object obj = pjp.proceed();
            if (args != null)
            {
                cache.put(Arrays.asList(args), obj);
            }
            return obj;
        }
    }
    
    
    
Now, add @Memoize to any method whose request and response should be cached and it just works. BTW, this is a very naive Memoize implementation and is not production ready at all ;-)