I am new to Java world with just 2 years approximately in my career. Recently I was working on a project that required us to minify JavaScript and CSS files in Java WAR archive. The technology stack for this project included Spring MVC, Hibernate, Gradle, BootStrap, and Thymeleaf.
After searching for a plugin that enables us to minify CSS and JavaScript files in gradle during dev/prod continuous integration build process, i ended up using a plugin from Eric Wendelin.
JavaScript files in our code base were not enabled with a structured namespace hierarchy. This omitted single minified file option for the project. Due to this constraint we decided minifying each file while keeping them separate.
Although you can find many examples online but making them work for your needs was not readily available and it took some research to get a working WAR with minified files.
Things to learn in this exercise
- How to define dependencies (JavaScript and CSS) on the plugin used to minify.
- How to define source directories.
- How to define custom gradle tasks dynamically for minifying JavaScript and CSS files recursively.
- How to define dependency on a task and execute dynamic tasks.
- How to pack a war with correct files while excluding unwanted file/folders.
How to define dependencies (JavaScript and CSS) on the plugin used to minify
The below given code snippet is from build.gradle file in your web application. This file will contain all changes we discuss in this exercise. To use plugin for minification you will need to add dependencies (highlighted below) to the plugins for JS and CSS.
*Please check plugin website for any plugin updates.
buildscript { repositories { maven { url "http://repo.spring.io/libs-milestone" } mavenLocal() } dependencies { classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.2.3' classpath 'com.eriwen:gradle-js-plugin:1.12.1' classpath 'com.eriwen:gradle-css-plugin:1.11.1' } } apply plugin: 'js' apply plugin: 'css'
How to define source directories
Although there are other ways to define source directory, we will be using a custom source definition for JavaScript and CSS folders. The “**/*.js” or “**/*.css” refers to include all files and sub-directories under js or css folder.
javascript.source { custom { js { srcDir 'src/main/webapp/WEB-INF/static/js' include "**/*.js" } } } css.source { custom { css { srcDir 'src/main/webapp/WEB-INF/static/css' include "**/*.css" } } }
How to define custom gradle tasks dynamically for minifying JavaScript and CSS files recursively
The below given code loops through each file it finds in our custom source directory. If you remember in the previous step we declared source folder to include all *.js and *.css files recursively from source directories js and css.
Line 2 creates new tasks for each file. To keep each task name unique index was appended to task name which is “dominifyJs” or “dominifyCss” in below given example.
As we are traversing in a folder structure which has sub-folders, Line 3 checks for parent folder to be js or create sub-folder. Source in Line 4 defines the path to pick source. Line 5 defines destination file path.
javascript.source.custom.js.files.eachWithIndex { jsFile, idx -> tasks.create(name: "dominifyJs${idx}", type: com.eriwen.gradle.js.tasks.MinifyJsTask) { if (jsFile.getParentFile().getName() != "js") { source = jsFile dest = "${buildDir}/tmp/js/" + jsFile.getParentFile().getName() + "/${jsFile.name}" closure { compilationLevel = 'SIMPLE_OPTIMIZATIONS' } } else { source = jsFile dest = "${buildDir}/tmp/js/${jsFile.name}" closure { compilationLevel = 'SIMPLE_OPTIMIZATIONS' } } } } css.source.custom.css.files.eachWithIndex { cssFile, idx -> tasks.create(name: "dominifyCss${idx}", type: com.eriwen.gradle.css.tasks.MinifyCssTask) { if (cssFile.getParentFile().getName() != "css") { source = cssFile dest = "${buildDir}/tmp/css/" + cssFile.getParentFile().getName() + "/${cssFile.name}" closure { compilationLevel = 'SIMPLE_OPTIMIZATIONS' } } else { source = cssFile dest = "${buildDir}/tmp/css/${cssFile.name}" closure { compilationLevel = 'SIMPLE_OPTIMIZATIONS' } } } }
How to define dependency on a task and execute dynamic tasks
Line 1 of code snippet below creates a new task individualJsMinify. This task depends on each task that starts with name “dominifyJs”. Similarly line 2 creates a new task individualCssMinify which depends on each task that starts with name “dominifyCss”.
task individualJsMinify(dependsOn: tasks.matching { Task task -> task.name.startsWith("dominifyJs") }) task individualCssMinify(dependsOn: tasks.matching { Task task -> task.name.startsWith("dominifyCss") })
How to pack a war with correct files while excluding unwanted file/folders
The last step is to declare things to be included and excluded from WAR file. Line 5 is getting all system properties, System properties can be set by -D command line parameter in gradle tomcatRun.
It can also be set in Continuous integration build server for skipping minification of JavaScript and CSS files in development environment. Line 6 is pulling a variable “spring.profiles.active” from System Properties.
The default value is set as “local” in this example (2nd argument of getProperty() function). If no such property is found it sets executionType as “local”.
If executionType is not “local”, war will execute minification process. When executionType is not “local” war build depends on two tasks we created in earlier steps, line 10 declares this dependency.
Line 11 and 12 excludes JavaScript, CSS source folders and sub-folders. They are non-minified version of files and will not be required.
Note: If you have sub-folders, each will have to be specified individually.
- When sub-folders were not specified for exclusion, a copy of these folders along with other source folders were packed in the root directory of WAR archive. This increased size of WAR file.
- Also if you view don’t exclude (source code: line 11, 12 ) non-minified versions you will see two references of same JavaScript/CSS file. One will show size as non-minified version while other will show size of minified version. This will not allow WAR to be identified as a valid web application file. You can view contents of a WAR file using below given command line.
jar tvf web-archive-test.war
Line 25 and 30 defines path where JavaScript and CSS files are to be picked for WAR archive. You can see that they are referring to build folder where minified version of these files were created earlier. Include on line 26 and 31 defines all files and folder with their respective extensions.
Into on line 27 and 32 defines path where they should be copied in WAR archive.
war { baseName = 'sample-web-app' version = '0.1.0' Properties systemProperties = System.getProperties(); String executionType = systemProperties.getProperty("spring.profiles.active", "local"); println executionType; if (!executionType.equalsIgnoreCase('local')) { dependsOn 'individualJsMinify', 'individualCssMinify' exclude 'WEB-INF/static/js/*' exclude 'WEB-INF/static/css/*' from('src/main/webapp/WEB-INF/static/js') { exclude '**/*.js' exclude 'sub-folderJs1/' exclude 'sub-folderJs2/' } from('src/main/webapp/WEB-INF/static/css') { exclude '**/*.css' exclude '*.map' exclude 'sub-folderCss1/' exclude 'sub-folderCss2/' } from('build/tmp/js') { include '**/*.js' into 'WEB-INF/static/js/' } from('build/tmp/css') { include '**/*.css' into 'WEB-INF/static/css/' } } }
Finally, we have reached end of this post. Please let me know if you have any further queries.