12 July, 2011

JBoss Application Server Deployment: Yeah, it's that easy.

As you may know JBoss Application Server 7 was released 12.July.2011. What you may not know is there is a wonderful and very easy to use Java API to help deploy your applications to a running server.

While you are able to deploy to a domain server here we will concentrate on deploying to a standalone server. For this example we'll be using Maven so you can use the source editor of your choice. Please see https://github.com/jamezp/jboss-as-custom-deployment-plugin to see the complete source for the examples.

First things first, we need to download and install JBoss Application Server 7 which can be found at http://www.jboss.org/as7. You can also find documentation on that page to learn more about the exciting new features that are in JBoss Application Server 7.

After JBoss Application Server 7 is installed, we need to setup our project. Below is a snippet of the dependencies needed in the POM.

        ...
        
            org.jboss.as

            jboss-as-controller-client
            ${as.version}
        
        ...
    
Now that we have our dependencies setup, let's write a custom deployment class. In this class we want to be able to connect to the running server and execute a deployment plan. There will be three deployment options; deploy, undeploy and redeploy. We will also need the archive file to deploy. Please note the code sample below is not complete and should be viewed at the github link above.

final class CustomDeployment {

    private final String hostname;
    private final int port;
    private final File archive;
    private final Type type;

    private final List<Throwable> errors = new LinkedList<Throwable>();

    CustomDeployment(final String hostname, final int port, final File archive, final Type type) {
        this.hostname = hostname;
        this.port = port;
        this.archive = archive;
        this.type = type;
    }

    static CustomDeployment of(final String hostname, final int port, final File archive, final Type type) {
        return new CustomDeployment(hostname, port, archive, type);
    }

    public Status execute() throws IOException {
        Status status = Status.SUCCESS;
        final ModelControllerClient client = ModelControllerClient.Factory.create(hostname, port);
        final ServerDeploymentManager manager = ServerDeploymentManager.Factory.create(client);
        final DeploymentPlanBuilder builder = manager.newDeploymentPlan();
        final DeploymentPlan plan;
        switch (type) {
            case DEPLOY:
                plan = builder.add(archive).deploy(archive.getName()).build();
                break;
            case REDEPLOY: {
                plan = builder.replace(archive).redeploy(archive.getName()).build();
                break;
            }
            case UNDEPLOY: {
                plan = builder.undeploy(archive.getName()).remove(archive.getName()).build();
                break;
            }
            default:
                plan = null;
                break;
        }
        if (plan == null) {
            throw new IllegalStateException("Invalid type: " + type);
        }

        if (plan.getDeploymentActions().size() > 0) {
            try {
                final ServerDeploymentPlanResult planResult = manager.execute(plan).get();
                // Check the results
                for (DeploymentAction action : plan.getDeploymentActions()) {
                    final ServerDeploymentActionResult actionResult = planResult.getDeploymentActionResult(action.getId());
                    final ServerUpdateActionResult.Result result = actionResult.getResult();
                    switch (result) {
                        case FAILED:
                        case NOT_EXECUTED:
                        case ROLLED_BACK: {
                            errors.add(actionResult.getDeploymentException());
                            status = Status.FAILURE;
                            break;
                        }
                        case CONFIGURATION_MODIFIED_REQUIRES_RESTART:
                            // Should show warning
                            break;
                        default:
                            break;
                    }
                }

            } catch (InterruptedException e) {
                errors.add(e);
                status = Status.FAILURE;
            } catch (ExecutionException e) {
                errors.add(e);
                status = Status.FAILURE;
            }
        }
        return status;
    }

    public Collection<Throwable> getErrors() {
        if (errors.isEmpty()) {
            return Collections.emptyList();
        }
        return Collections.unmodifiableCollection(errors);
    }

    public File getArchive() {
        return archive;
    }

    public String getHostname() {
        return hostname;
    }

    public int getPort() {
        return port;
    }

    public Type getType() {
        return type;
    }
}
Simple enough right? Pay careful attention to the execute() method as that is where our deployment logic is. You can see based on the Type we create a different deployment plan with the builder. We then execute the plan via the ServerDeploymentManager.execute() method, which returns a Future. For this example we'll just block using the Future.get() method until the plan has been fully executed. Once the plan has been executed and the manager returns the Future, we'll process the results and check for errors.

Finally we need to create some way to execute this custom deployment class. We will create a simple command line utility that accepts arguments to execute different deployment options.
public class Main {
    private static final String PARAM_USAGE_FORMAT = "   %-20s - %s%n";

    public static void main(final String[] args) throws IOException {
        String hostname = "localhost";
        int port = 9999;
        File archive = null;
        Type type = DEPLOY;

        // At least one parameter must be passed
        if (args == null || args.length < 1) {
            printUsage();
            System.exit(1);
        }

        // Parse incoming parameters
        for (int i = 0; i < args.length; i++) {
            final String arg = args[i];
            if ("-hostname".equalsIgnoreCase(arg)) {
                hostname = args[++i];
            } else if ("-port".equalsIgnoreCase(arg)) {
                try {
                    port = Integer.parseInt(args[++i]);
                } catch (NumberFormatException e) {
                    System.out.printf("Error: %s%n%n", e.getMessage());
                    printUsage();
                    System.exit(1);
                }
            } else if ("-type".equalsIgnoreCase(arg)) {
                final String s = args[++i];
                if (Type.isValid(s)) {
                    type = Type.parse(s);
                } else {
                    System.out.printf("Type %s is an invalid type.%n%n", s);
                    printUsage();
                    System.exit(1);
                }
            } else {
                // arg should be a file and the file must exist
                final File file = new File(arg);
                if (file.exists()) {
                    archive = file;
                } else {
                    System.out.printf("File %s does not exist.%n%n", file.getAbsolutePath());
                    printUsage();
                    System.exit(1);
                }
            }
        }

        // Create the custom deployment
        final CustomDeployment deployment = new CustomDeployment(hostname, port, archive, type);
        switch (deployment.execute()) {
            case SUCCESS: {
                System.out.printf("Deployment was successful for archive %s.%n", deployment.getArchive());
            }
            case FAILURE: {
                System.out.printf("Deployment failed for archive %s.%n", deployment.getArchive());
                System.out.println("  Errors:");
                for (Throwable t : deployment.getErrors()) {
                    System.out.printf("   %s%n", t.getMessage());
                }
                System.exit(1);
            }
            default: {
                System.out.println("Invalid response from the deployment.");
            }
        }
        System.exit(0);
    }

    private static void printUsage() {
        System.out.println("Usage:");
        System.out.printf(PARAM_USAGE_FORMAT, "-hostname <hostname>", "The host name to connect to. Default is localhost.");
        System.out.printf(PARAM_USAGE_FORMAT, "-port <port>", "The port to connect to. Default is 9999.");
        System.out.printf(PARAM_USAGE_FORMAT, "-type <type>", "The type of the deployment. Valid values are: DEPLOY, REDEPLOY and UNDEPLOY. The default is DEPLOY");
        System.out.printf(PARAM_USAGE_FORMAT, "archiveFile", "The archive file to execute the deployment type on.");
    }
}
As you can see this class simply parses command line arguments, creates a CustomDeployment and invokes the execute() method. We monitor for errors and print the messages accordingly.

In the end this exercise was mainly to show you how easy the JBoss AS Native Java Detyped Management API is for JBoss Application Server 7. There are several ways for you to deploy your applications to the server. Using the JBoss AS Native Java Detyped Management API is just one option. Using this option might be appealing if you have a complicated or non-standard build the you want to write custom deployment plans for.

Please see http://www.jboss.org/as7 for more details. Download, run it and see "How #@*%ing fast" it really is.