Unable to run repacked spring boot jar caused by "Unable to open nested entry" - spring-boot

I am setting up a build pipeline for a spring boot project.
It has three stages so far:
build: compile-->unit test-->archive the jar
deploy acceptance test: repack the jar for acc environment (replacing datasource.properties etc)
deploy uat test: repack the jar for uat environment (replacing datasource.properties etc)
I don't want to build the jar from scratch for different environments as it wastes time and potentially has risk of building inconsistent artifacts.
For traditional war project, I just extract the war, replace the config files and repack. But this time with spring boot, somehow it does not work. When I run the repacked jar, it reports
java.lang.IllegalStateException: Unable to open nested entry 'lib/antlr-2.7.7.jar'. It has been compressed and nested jar files must be stored without compression. Please check the mechanism used to create your executable jar file
at org.springframework.boot.loader.jar.JarFile.createJarFileFromFileEntry(JarFile.java:378)
at org.springframework.boot.loader.jar.JarFile.createJarFileFromEntry(JarFile.java:355)
at org.springframework.boot.loader.jar.JarFile.getNestedJarFile(JarFile.java:341)
at org.springframework.boot.loader.archive.JarFileArchive.getNestedArchive(JarFileArchive.java:108)
at org.springframework.boot.loader.archive.JarFileArchive.getNestedArchives(JarFileArchive.java:92)
at org.springframework.boot.loader.ExecutableArchiveLauncher.getClassPathArchives(ExecutableArchiveLauncher.java:68)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:60)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:45)
I extracted the origin jar and the repacked jar and don't find differences with lib folder.
task extractArtifact() {
doLast {
def outputDirName = "${buildDir}/tmp/under_config"
def outputDir = file(outputDirName)
assert outputDir.deleteDir() // cleanup workspace
def zipFile = file("${buildDir}/libs/${getArtifactName()}")
copy {
from zipTree(zipFile)
into outputDir
}
copy {
from file("${buildDir}/env")
into file("${buildDir}/tmp/under_config")
}
}
}
task repackConfiguredArtifact(type: Zip, dependsOn: extractArtifact) {
archiveName = "${getArtifactName()}"
destinationDir = file("${buildDir}/libs/${getEnv()}")
from file("${buildDir}/tmp/under_config")
}
Does anyone have an idea?
Or how do you guys config the jar for different environment (without re-compile the binary).

You shoud add -0 to store only; use no ZIP compression
$jar -cvf0m yourproject.jar META-INF/MANIFEST.MF .
There is another soulution:
Set the active Spring profiles
$java -jar -Dspring.profiles.active=production demo-0.0.1-SNAPSHOT.jar
You can use application-${profile}.properties to specify profile-specific values.

I have a solution after looking up the spring-boot reference.
turn default spring boot repackage off since I need to repack it anyway.
extract the traditional jar and copy the config files
Use jar type task to repack it
Use BootRepackage type task to assemble a spring-boot jar.
here is the code:
bootRepackage {
enabled = false
}
task extractArtifact() {
doLast {
def outputDirName = "${buildDir}/tmp/under_config"
def outputDir = file(outputDirName)
assert outputDir.deleteDir() // cleanup workspace
def zipFile = file("${buildDir}/libs/${getArtifactName()}")
copy {
from zipTree(zipFile)
into outputDir
}
copy {
from file("${buildDir}/env")
into file("${buildDir}/tmp/under_config")
}
assert zipFile.delete()
}
}
task clientJar(type: Jar, dependsOn: extractArtifact) {
archiveName = "${getArtifactName()}"
from file("${buildDir}/tmp/under_config")
}
task repackConfiguredArtifact(type: BootRepackage, dependsOn: clientJar) {
withJarTask = clientJar
}

I found that if you had a directory called 'resources/lib' the spring-boot executable JAR would assume the contents were zipped and throw the above exception. Renaming to 'resources/static' worked for me.

Related

Gradle Ear plugin should not copy resources into .ear root

I have the following folder structure of my gradle module:
src
main
application
META-INF
application.xml
was.policy
was.webmodule
java
resources
image.bmp
logback.xml
... other files, properties
webapp
My goal is to build an ear archive, which will contain only Tclient.war and META-INF. However, gradle copies all resource files to the ear root.
Gradle documentation about Ear Plugin says that:
The default behavior of the Ear task is to copy the content of
src/main/application to the root of the archive. If your application
directory doesn’t contain a META-INF/application.xml deployment
descriptor then one will be generated for you.
So, It's not really clear why it puts resources into the root. Maybe, it work's like the org.gradle.api.tasks.bundling.Jar task and I should override this behaviour in some way?
Here is my partially build.gradle file:
jar {
description 'Creates tclient.jar'
archiveBaseName = 'tclient'
destinationDirectory = file('src/main/webapp/WEB-INF/lib')
from sourceSets.main.output
include '**/*'
include '**/*.properties'
include '**/*.cmd'
}
ear{
description = "Ear archive for WebSphere server"
archiveBaseName = 'Tclient'
appDirName('src/main/application')
// workaround to exclude classes from ear root
rootSpec.exclude('**/de/**')
rootSpec.exclude('**/org/**')
}
war {
archiveFileName = 'Tclient.war'
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
webInf {
from configurations.natives into 'lib/bin'
}
// do not put compiled classes inside WEB-INF/classes
rootSpec.exclude('**/de/**')
rootSpec.exclude('**/org/**')
rootSpec.exclude('urlrewrite*.dtd')
from(jar) {
into 'WEB-INF/lib'
}
}
dependencies {
// Place .war inside .ear root
deploy files(war)
....
}
The possible workaround is to exclude redundant resources from the rootspec:
ear{
description = "Ear archive for WebSphere server"
archiveBaseName = 'Tclient'
appDirName('src/main/application')
// workaround to exclude classes from ear root
rootSpec.exclude('**/de/**')
rootSpec.exclude('**/org/**')
// workaround
rootSpec.exclude('*.properties')
rootSpec.exclude('*.xml')
rootSpec.exclude('*.dtd')
rootSpec.exclude('*.bmp')
}
However, I'm finding this workaround a little bit ugly and it's a dirty hack.

Gradle, copy and rename file

I'm trying to in my gradle script, after creating the bootJar for a spring app, copy and rename the jar that was created to a new name (which will be used in a Dockerfile). I'm missing how to rename the file (I don't want to have the version in docker version of the output file).
bootJar {
baseName = 'kcentral-app'
version = version
}
task buildForDocker(type: Copy){
from bootJar
into 'build/libs/docker'
}
You could directly generate the jar with your expected name, instead of renaming it after it has been generated, using archiveName property from bootJar extension:
bootJar {
archiveName "kcentral-app.jar" // <- this overrides the generated jar filename
baseName = 'kcentral-app'
version = version
}
EDIT
If you need to keep the origin jar filename (containing version), then you can update your buildForDocker task definition as follows:
task buildForDocker(type: Copy){
from bootJar
into 'build/libs/docker'
rename { String fileName ->
// a simple way is to remove the "-$version" from the jar filename
// but you can customize the filename replacement rule as you wish.
fileName.replace("-$project.version", "")
}
}
For more information, see Gradle Copy Task DSL
You can also do something like this:
task copyLogConfDebug(type: Copy){
group = 'local'
description = 'copy logging conf at level debug to WEB-INF/classes'
from "www/WEB-INF/conf/log4j2.debug.xml"
into "www/WEB-INF/classes/"
rename ('log4j2.debug.xml','log4j2.xml')

How to use Gretty integrationTestTask with a war file?

Is it possible to use gretty integrationTestTask with a project that uses a war folder?
It seems from the documentation appBeforeIntegrationTest does not have access to the war. Is there another way to run test cases so that it uses the war folder?
Ideally, I want jettyStart -> test -> jettyStop to run. Although when I run it straight jettyStart hangs indefinitely, until jettyStop is run. Is there a way to run jettyStart in Gradle in the background or something?
Regardless what file structure your application has, the integrationTestTask is supposed to be configured with the name of an exsiting gradle task to execute when gradle integrationTest is run:
gretty {
// ...
integrationTestTask = 'integrationTest' // name of existing gradle task
// ...
}
What you want to archive is this:
gretty {
integrationTestTask = 'test'
}
Gretty's workflow when calling integrationTest is as follows:

gradle ear application packaging for multiple environment

I have a web enterprise application that I am trying to automate building the EAR file for it with gradle (for the first time).
I have 2 projects
1 is the enterprise application APP_EAR and the other one is a dynamic web application APP that will be exported as a war file inside the main APP_EAR ear file.
I figured out how to use gradle to generate a war file for the dynamic web app, I am not sure how to use gradle to create the ear file that contains this war file.
The war file for my APP contains 3 tasks for each environment:
sys (system testing), qa (quality control) and prod for production
I created 3 tasks in the build.gradle for my APP like this:
task buildSys {
dependsOn copySys, war
}
task buildQa {
dependsOn copyQa, war
}
task buildProd {
dependsOn copyProd, war
}
the copySys, copyProd, etc... are just copy tasks that copies the appropriate configuration files for the environment I am targeting, before the war is created.
let's say I am creating a war file for sys , then I execute gradle buildSys
What I am trying to do is the following:
Assuming I want to create an EAR file for sys environment, I need to use gradle to perform those steps:
1) execute buildSys in the APP project to create a war file for sys environment
2) execute buildSysEAR that will package the created war file into an ear file.
I am not sure if I need to have 3 tasks in my build.gradle for APP_EAR to package the ear file for each environment or whether I can do it with one ear task and make it call the appropriate war task for that environment.
I just want to be able to execute one task that builds the war and the ear for this environment, I am not sure how to tie the war file to the ear file and/or trigger the correct build task for the war file from the APP_EAR build file.
Thank you in advance
Gradle supports creating an EAR using a plugin. You specify the WAR as a dependency and it will create the necessary files for you. (There is also a war plugin, it's simpler)
apply plugin: "ear"
dependencies
{
deploy project("some support project") // only necessary for ejb jars
earlib "log4j:log4j:1.2.17:jar"
}
ear
{
appDirName "EarContent" // or wherever, defaults to src/main/application
deploymentDescriptor
{
displayName = "app-ear"
description = "my application"
web = project("APP")
value = web.name + '-' + web.version + ".war"
webModule( value , "/web-context") // this is the important line, I need the version number
}
}
Assume project A (EAR Project) has a build.gradle and within ear, the below code can be solve this - in this example assuming 'B' is a dynamic web application need to deploy within project 'A' -
plugins {
id 'ear'
}
dependencies {
deploy project(path:':b', configuration: 'archives')
}
ear{
/* some basis configuration */
libDirName 'APP-INF/lib'
deploymentDescriptor {
/* Some basic attributes */
fileName = "application.xml"
version = "8"
def Set<Project> subProj = project.getSubprojects();
subProj.each{proj ->
if(proj.name.contains("B")){
webModule(proj.name + "-" + proj.version + ".war", "/"+ proj.name)
} //if close
}//each close
}//deploymentDescriptor close
}//ear close

Filter out resources from custom Test task in Gradle with goal of having one properties file for tests and a different one for production

So I have setup a way to just run integration tests using this configuration:
test {
exclude "**/*IntegrationTest*.class"
}
task integrationTest(type: Test, dependsOn: testClasses) {
include "**/*IntegrationTest*.class"
}
check.dependsOn integrationTest
Works great. But then for logging in my integration tests, I want to use a log4j.properties file from a specific directory instead of the one that is located in the src/main/resoures which is used for production.
I've tried this but didn't work:
integrationTest.classpath = files("$rootDir/test/src/main/resources/log4j.properties") + integrationTest.classpath
I also tried to just see if I could exclude the file, but could not find a way. Tried this:
processTestResources.exclude "**/*log4j.properties"
Any suggestions for including one properties file in production and another one for tests?
If you put the 'test' log4j.properties in the src/test/resources directory, it will actually come before anything in src/main/resources on the classpath of your tests/integration tests.
An alternative solution is to setup your bundling so that the log4j.properties for production is not in src/main/resources, but is added to the jar from a different directory...
jar {
from('production') {
include('log4j.properties')
}
}
If you want to keep the log4j.properties files in their current locations, you were almost there with what you tried. On the classpath you can have either jar files, or directories containing resources. So try doing this:
integrationTest.classpath = files("$rootDir/test/src/main/resources") + integrationTest.classpath
Seems like you could do something like this also so that the .jar task will include it later:
sourceSets {
main {
java {
srcDir 'src/main/java'
output.classesDir = 'build/classes/main'
}
resources {
srcDir 'src/main/resources'
include 'logback.xml'
output.resourcesDir = 'build/resources/main'
}
}
}

Resources