Showing posts with label Spring. Show all posts
Showing posts with label Spring. Show all posts

Wednesday, May 27, 2015

Deploying Spring Boot App using SCP

        In this article Iam going to cover how you could deploy a spring boot application jar into remote server.
I would assume that you have spring boot application is all set up and you could build and generate a deployable (executable jar) artifact using maven. In boot once we have this jar we could simply issue a java command on this jar to start the application inside an embedded tomcat.
So technically deploying the jar artifact means simply uploading the jar to specified location on remote server.We will be using maven-wagon plugin and wagon-ssh extension connector in this article to upload this jar artifact into specified directory on remote linux server as part of maven deploy phase.

In order for this plugin to work we will need to configure the remote server information in the maven settings.xml like below,

<server>
   <id>my.remote.server</id>
   <username>myname</username>
   <password>mypassword</password>
</server>

In the application pom file add the wagon-ssh extension as shown below into the build section,
<extensions>
 <extension>
  <groupId>org.apache.maven.wagon</groupId>
  <artifactId>wagon-ssh</artifactId>
  <version>2.10</version>
 </extension>
</extensions>

Make sure the wagon version is 2.9 and above.
Now add the plugin into your application pom as shown below,
<plugin>
 <groupId>org.codehaus.mojo</groupId>
 <artifactId>wagon-maven-plugin</artifactId>
 <version>1.0</version>
 <executions>
  <execution>
   <id>upload</id>
   <phase>deploy</phase>
   <goals>
    <goal>upload-single</goal>
   </goals>
   <configuration>
    <serverId>my.remote.server.id</serverId>
    <fromFile>${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}</fromFile>
    <url>scp://my.remote.server/opt/apps</url>
   </configuration>
  </execution>
 </executions>
</plugin>

We just added a wagon plugin and defined a new goal into the maven's deploy phase.
We will use upload-single goal from wagon plugin since we want to upload only one file which is the application jar file.
Notice the configuration piece for this execution goal upload.

In the url you notice that it is trying to upload into /opt/apps directory. Instead you could add toDir attribute if you want.

It is very important to use the serverId that matches the id of server configuration you did in your maven settings earlier. If you don't specify couple of issues you will run into,
First it will pick up default maven user and forms the default url as "mavenUser@my.remote.server"
If the user is same on remote host then no issues but it would be different in most cases.
So now if you try to modify your url in the configuration as "remoteUser@my.remote.server" then it will end up prompting for the password in the middle of the build that you don't want. So simply point your plugin's configuration to maven server settings using serverId attribute.

Now in order to run maven deploy command you will need to provide the repository distribution url like as shown below,
<distributionManagement>
   <repository>
 <id>my.remote.server.id</id>
 <url>scp://my.remote.server/opt/apps</url>
   </repository>
</distributionManagement>

That's it, now you can run command

 mvn deploy

Your application should run through all phases of build cycle and then finish up successfully uploading the jar file onto remote host.

At this point you should be able to login to your remote server and issue a boot start command.
java -jar mySpringBootApp.jar

While you are in the remote server, if you list the contents under /opt/apps at this point
you will see application jar file and also a folder (example com or org) based on whatever the package structure you used in your application's maven groupId coordinate. You wonder how?, what happened here was maven's default deploy plugin kicked in, generated maven meta data, pom with the deployed application's coordinates and it uploaded it to remote location along with the jar.
In case you really don't want this additional folder in your remote host created by maven's default deploy plugin you simply turn the default deploy plugin off, I mean skip it as shown below,

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-deploy-plugin</artifactId>
    <configuration>
 <skip>true</skip>
    </configuration>
</plugin>

If you decide to keep this additional info folder then make sure you are using wagon-ssh version 2.9 and above but if you are using older versions then you will run into issues with redeploy. Build will simply hangs while uploading the meta data. This is due to some glitches either in the maven version or wagon-ssh connection version.
If you are using maven 3.3.x and wagon-ssh version 2.9 and above but problem still persist then you should look into switching to wagon-ssh-external.
Here is how you can replace the extension connector,

<extension>
     <groupId>org.apache.maven.wagon</groupId>
     <artifactId>wagon-ssh-external</artifactId>
     <version>2.6</version>
</extension>

Then you will have to change the URL in your plugin configuration as well as repository distribution section to something like this,
scp://my.remote.server

to 

scpexe://my.remote.server

Lastly if you want to just run the command,
>mvn wagon:upload-single

You can't at this time since the configuration for the plugin is inside the execution so we need to add the same configuration outside of execution as well,
<plugin>
        <groupId>org.codehaus.mojo</groupId>
 <artifactId>wagon-maven-plugin</artifactId>
 <version>1.0</version>
 <configuration>
  <serverId>192.168.88.128</serverId>
  <fromFile>${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}</fromFile>
  <url>scp://192.168.88.128/opt/apps</url>
 </configuration>
 <executions>
  <execution>
   <id>upload</id>
   <phase>deploy</phase>
   <goals>
    <goal>upload-single</goal>
   </goals>
   <configuration>
    <serverId>192.168.88.128</serverId>
    <fromFile>${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}</fromFile>
    <url>scp://192.168.88.128/opt/apps</url>
   </configuration>
  </execution>
 </executions>
</plugin>

Now you could run both,
>mvn clean deploy

or

>mvn clean install wagon:upload-single

Here is the complete pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>org.test</groupId>
 <artifactId>demoboot</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>demoBoot</name>
 <description>Demo project for Spring Boot</description>

 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.2.3.RELEASE</version>
  <relativePath /> 
 </parent>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <start-class>demoboot.DemoBootApplication</start-class>
  <java.version>1.7</java.version>
 </properties>
 
 <dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
  </dependency>
 </dependencies>

 <build>
  <extensions>
   <extension>
    <groupId>org.apache.maven.wagon</groupId>
    <artifactId>wagon-ssh</artifactId>
    <version>2.10</version>
   </extension>
  </extensions>

  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-deploy-plugin</artifactId>
    <configuration>
     <skip>true</skip>
    </configuration>
   </plugin>
   <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>wagon-maven-plugin</artifactId>
    <version>1.0</version>
    <configuration>
     <serverId>192.168.88.128</serverId>
     <fromFile>${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}</fromFile>
     <url>scp://192.168.88.128/opt/apps</url>
    </configuration>
    <executions>
     <execution>
      <id>upload</id>
      <phase>deploy</phase>
      <goals>
       <goal>upload-single</goal>
      </goals>
      <configuration>
       <serverId>192.168.88.128</serverId>
       <fromFile>${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}</fromFile>
       <url>scp://192.168.88.128/opt/apps</url>
      </configuration>
     </execution>
    </executions>
   </plugin>
  </plugins>

 </build>

 <distributionManagement>
  <repository>
   <id>192.168.88.128</id>
   <url>scp://192.168.88.128/opt/apps</url>
  </repository>
 </distributionManagement>


</project>

That's it for now, happy deploying spring boot.

References

Learn more about wagon-ssh
Learn more about maven deploy plugin.
Here is stackoverflow thread talks about fixing hanging issue with wagon-ssh older version

Monday, December 23, 2013

Springify The Strategy Pattern - Part 2


I talked about how to implement strategy pattern in springapplication in my previous post. Here I want to show you how Spring Annotations can make our lives easier to implement these patterns.
Spring 2.5 version introduced way to configure the dependency injection using annotations. This eliminates all those XML files in an application that just defined beans and bean wiring. Now annotation allows you to configure these inside the bean (component).

Some of the annotations that I will be using were @Autowired, @Resource, @Component, @Service etc.
All right let’s get into the code, I will take the second approach from the previous article and configure it using annotation. The new configuration XML looks like this,

<context:component-scan base-package="xyz.xyz.strategy"/>

What happened to all bean definitions? What is this component scan?
Well annotation based configuration eliminated the bean definition in the XML. Instead we enable spring auto component scanning of components in the base package where it can scan, detect, instantiate and wire all these beans (components).

Now let’s look at the beans again. You will have to go back to your strategy implementation classes like RegularShippingCostStrategyImpl etc. and add @Service or @Component annotation to the class as shown below,
@Service
public class RegularShippingCostStrategyImpl implements ShippingCostStrategy {
//calculation code
}

@Service
public class PremiumShippingCostStrategyImpl implements ShippingCostStrategy {
//calculation code
}

@Service
public class PrimeShippingCostStrategyImpl implements ShippingCostStrategy {
//calculation code
}

 
Your ShippingCostService class with added annotation below,
@Service
public class ShippingCostService {
 
 @Autowired
 private ShippingCostContext shippingContext = null;
  
 public double calculateShipping(Item item, String memberStatus){
  double cost = shippingContext.calculateShipping(item, memberStatus);
     return cost;
 }
 
}

 
Now look at this strategy context class,
@Component
public class ShippingCostContext {
 
 @Autowired
 private PrimeShippingCostStrategyImpl primeShippingStrategy = null;
 
 @Autowired
 private PremiumShippingCostStrategyImpl premiumShippingStrategy = null;
 
 @Autowired
 private RegularShippingCostStrategyImpl regularShippingStrategy = null;
        
 public double calculateShipping(Item item, String memberStatus){
       double cost = 0.0;
       if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_PRIME))
          cost = primeShippingStrategy.calculate(item);
       else if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_PREMIUM))
          cost = premiumShippingStrategy.calculate(item);
       else if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_REGULAR))
          cost = regularShippingStrategy.calculate(item);
    
       return cost;
 }
   
}

Now run the same test to check this out.
The same can be achieved for the first approach as well by simply switching auto wire strategy implementation classes from strategy context class to service class.
About the third approach is not all required since spring auto scan and auto wire simplifies the implementation already. But in case if you are interested here is how you can do it.

<context:component-scan base-package="xyz.xyz.strategy"/>
   
<util:map id="strategies" map-class="java.util.HashMap">
       <entry>
         <key>
           <util:constant static-field=" xyz.xyz.strategy.StrategyConstants.MEMBER_REGULAR"/>
          </key>
            <ref bean="regularShippingCostStrategyImpl" />
         </entry>
         <entry>
             <key>
              <util:constant static-field=" xyz.xyz.strategy.StrategyConstants.MEMBER_PREMIUM"/>
             </key>
             <ref bean="premiumShippingCostStrategyImpl" />
         </entry>
         <entry>
             <key>
              <util:constant static-field=" xyz.xyz.strategy.StrategyConstants.MEMBER_PRIME"/>
             </key>
             <ref bean="primeShippingCostStrategyImpl" />
         </entry>
   </util:map>

The XML context will have to define the spring util Map with key and ref value as shown above. You will then have to Autowire the map into the strategy context like this,
@Service
public class ShippingCostContext {
 
 @Resource
 private Properties strategies;
 
 public double calculateShipping(Item item, String memberstatus){
        double cost = 0.0;
        if(strategies.containsKey(memberstatus)){
         ShippingCostStrategy shippingCostStrategy = (ShippingCostStrategy)strategies.get(memberstatus);
         if(shippingCostStrategy != null)
          cost = shippingCostStrategy.calculate(item);
        }
               
 return cost;
 }


That’s all, the strategy pattern is now spring annotated. The spring annotations are quite powerful and simplify the configuration.