Saturday, November 10, 2012

Getting started with Spring Shell

I thought I'd write up a quick getting-started guide for Spring Shell. Spring Shell is the new Spring portfolio project that helps you in building easy to use command line interfaces for whatever commands you provide. Commands can really be anything. For instance, on most projects the developers end up writing a bunch of tools and utilities automating tedious tasks such setting up a database schema, scanning through log files or doing some code generation. All of these would be perfect examples for what a Spring Shell command can be. Using Spring Shell to house all of your tooling and utility commands in a coherent shell makes them self documenting and easier to use for other developers on the team.

Let's setup a trivial Spring Shell application to get started. In follow-up posts I'll cover more advanced Spring Shell functionality. I'll be using Maven in this example because I think that's what most people are familiar with (Spring Shell itself is built with Gradle).

Here's the POM for the spring-shell-demo project (available on GitHub):

<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>com.ervacon</groupId>
 <artifactId>spring-shell-demo</artifactId>
 <packaging>jar</packaging>
 <version>1.0-SNAPSHOT</version>
 <name>Spring Shell Demo</name>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>

 <repositories>
  <!-- see https://jira.springsource.org/browse/SHL-52 -->
  <repository>
   <id>ext-release-local</id>
   <url>http://repo.springsource.org/simple/ext-release-local/</url>
  </repository>
 </repositories>

 <dependencyManagement>
  <dependencies>
   <dependency>
    <groupId>org.springframework.shell</groupId>
    <artifactId>spring-shell</artifactId>
    <version>1.0.0.RELEASE</version>
   </dependency>
  </dependencies>
 </dependencyManagement>
 
 <dependencies>
  <dependency>
   <groupId>org.springframework.shell</groupId>
   <artifactId>spring-shell</artifactId>
  </dependency>
 </dependencies>
 
 <build>
  <plugins>
   <!-- copy all dependencies into a lib/ directory -->
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>2.1</version>
    <executions>
     <execution>
      <id>copy-dependencies</id>
      <phase>prepare-package</phase>
      <goals>
       <goal>copy-dependencies</goal>
      </goals>
      <configuration>
       <outputDirectory>${project.build.directory}/lib</outputDirectory>
       <overWriteReleases>true</overWriteReleases>
       <overWriteSnapshots>true</overWriteSnapshots>
       <overWriteIfNewer>true</overWriteIfNewer>
      </configuration>
     </execution>
    </executions>
   </plugin>
   
   <!-- make the jar executable by adding a Main-Class and Class-Path to the manifest -->
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.3.1</version>
    <configuration>
     <archive>
      <manifest>
       <addClasspath>true</addClasspath>
       <classpathPrefix>lib/</classpathPrefix>
       <mainClass>org.springframework.shell.Bootstrap</mainClass>
      </manifest>
     </archive>
    </configuration>
   </plugin>
  </plugins>
 </build>

</project>

There's quite a bit going on in this POM. First thing to note is the declaration of the ext-release-local SpringSource repository. This is needed to be able to resolve one of Spring Shell's dependencies: JLine (check SHL-52 for more background info). Hopefully this will no longer be necessary in future versions of Spring Shell. Next there's a dependency on Spring Shell itself. No surprise there. Finally, the POM customizes the build by copying all dependencies into a lib/ directory and adding a Main-Class and Class-Path property to the manifest of the generated jar file. Doing this produces the following directory structure in your target folder:

target/
 spring-shell-demo-1.0-SNAPSHOT.jar
 lib/
  all dependencies
You can simply package this up and distribute your shell. It will be fully self-contained and launching it is trivial:
java -jar spring-shell-demo-1.0-SNAPSHOT.jar

Hold on there, we're getting ahead of ourselves. Before launching the shell let's first add an echo command that just prints its input text back out to the console. Here's the code, which lives in the com.ervacon.ssd package:

@Component
public class DemoCommands implements CommandMarker {

 @CliCommand(value = "echo", help = "Echo a message")
 public String echo(
   @CliOption(key = { "", "msg" }, mandatory = true, help= "The message to echo") String msg) {
  return msg;
 }
}

As you can see, a Spring Shell command is just a @CliCommand annotated method on a Java class tagged with the CommandMarker interface. Our echo method takes a single argument which will be a mandatory @CliOption for the command. By using both the empty string and msg as keys for the option, you'll be able to invoke the command both as echo test and echo --msg test. The method simply returns the message: Spring Shell will make sure it gets printed to the console.

Alright, we've got our command implemented! We still have to tell Spring Shell about it. Since Spring Shell is Spring based, adding a command simply means defining a Spring bean. On start up, Spring Shell will automatically load the application context defined in classpath:/META-INF/spring/spring-shell-plugin.xml. You typically setup component scanning and simply mark your command classes as @Components, making development of new commands trivial since the shell will automatically detect them. Here's the application context definition:

<?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"
 xsi:schemaLocation="
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

 <context:component-scan base-package="com.ervacon.ssd" />

</beans>

And that's it! We're ready to build and launch our shell:

mvn package
java -jar target/spring-shell-demo-1.0-SNAPSHOT.jar

2 comments:

  1. Hi Erwin,
    I'm playing with Spring Shell, but something that I could not figure it out is the LOG during the startup and shutdown of the Spring Shell:

    java -jar target/spring-shell-demo-1.0-SNAPSHOT.jar
    Mar 04, 2014 9:47:20 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
    INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@d90727: startup date [Tue Mar 04 09:47:20 BRT 2014]; root of context hierarchy
    Mar 04, 2014 9:47:20 AM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
    :

    spring-shell>quit
    Closing org.springframework.context.support.ClassPathXmlApplicationContext@173180c: startup date [Tue Mar 04 09:51:17 BRT 2014]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@d90727
    Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@f13e82: defining beans [readContainerCmd,getSubscriberAccountDetailsCmd,removeContainerCmd,addContainerCmd,getSubscriberDetailsCmd,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,deleteSubscriberCmd,createSubscriberCmd,org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor#0]; parent: org.springframework.beans.factory.support.DefaultListableBeanFactory@1f7d3b1
    :

    I would appreciate for any help/tip around this.

    Thanks,

    ReplyDelete
  2. All of the Spring portfolio projects, including Spring Shell use commons-logging to abstract from the logging framework used by the application, i.e. Log4j or Logback. So to avoid this logging output, you need to do two things:
    1) make sure you have a proper logging framework on your classpath (i.e. Logback). Commons-logging will automatically start using it.
    2) configure it appropriately (i.e. using a logback.xml in case of Logback).

    ReplyDelete