I'm quite new to Java and Spring. I'm looking for a containerized solution that watches the src folder, rebuilds the project and takes advantage of Spring devtools hotswap to reload the changed classes.
I searched, but I just keep finding about production-ready containers, with separated steps for build and run. I tried to use 2 different containers, one with Gradle that keeps building (gradle build --continuous) and one that executes the built result:
version: '3.7'
services:
builder:
image: gradle:jdk11
working_dir: /home/gradle/project
volumes:
- ./:/home/gradle/project
command: gradle build --continuous
api:
image: openjdk:11-slim
volumes:
- ./build/classes:/app
command: java -classpath /app/java/main com.example.Application
It fails because Java doesn't find the dependencies (Spring, devtools, h2, etc.) inside the api container, and I don't know how to ask Gradle to include the external jars in the build folder. I want to do something like this, except that the example is outdated.
Still, I keep thinking that there might be a more elegant, simpler solution. It doesn't have to be with Gradle, it can be Maven if it works! :)
I know that many IDE have support for automatic builds and devtools, I just want to achieve it on Docker. This way, I would have a development workflow that is on repository, instead of on IDE's configuration, and virtually compatible with any dev environment. Is it a bad idea?
At last, I've found a solution that works quite well, with just one caveat.
This is of course a development environment, meant to quickly change files, automatically build and refresh the Spring application. It is not for production.
The build process is delegated to a Gradle container that watches for changes. Since Gradle has Incremental Compilation, it should scale well even for big projects.
The application itself is executed on a openjdk:11-slim. Since it runs the .class files, SpringBoot gets that it's dev-env and activates its devtools.
Here's my docker-compose.yml:
version: '3.7'
services:
builder:
image: gradle:jdk11
working_dir: /home/gradle/project
volumes:
- ./build:/home/gradle/project/build
- ./src:/home/gradle/project/src
- ./build.gradle:/home/gradle/project/build.gradle
command: gradle build --continuous -x test -x testClasses
api:
image: openjdk:13-alpine
volumes:
- ./build:/app
depends_on:
- builder
command: java -cp "/app/classes/java/main:/app/dependencies/*:/app/resources/main" com.example.Application
And here's my build.gradle:
plugins {
id 'org.springframework.boot' version '2.2.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.1.0-SNAPSHOT'
sourceCompatibility = '11'
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
task copyLibs(type: Copy) {
from configurations.runtimeClasspath
into "${buildDir}/dependencies"
}
build.dependsOn(copyLibs)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
All in all, it takes 5s for the whole thing to rebuild and hot-swap a change in the source code. Not webpack-like quick, but still acceptable. And the biggest advantage of having it this way is that it resides on code, and everyone can get it working, regardless of their workstation.
The caveat? On the first run, the build folder is empty and the api container fails to start. You have to wait for builder to complete its work, and then restart api.
I'm still hoping for a better solution, and I encourage you to post everything that works smoother than this.
Related
I tried to add an extension with command .\gradlew addExtension --extensions=io.quarkus:quarkus-resteasy-mutiny --stacktrace. I got the following error:
Caused by: org.gradle.api.GradleException: No platforms detected in the project
at io.quarkus.gradle.tasks.QuarkusPlatformTask.platformDescriptor(QuarkusPlatformTask.java:45)
at io.quarkus.gradle.tasks.QuarkusPlatformTask.getQuarkusProject(QuarkusPlatformTask.java:188)
at io.quarkus.gradle.tasks.QuarkusAddExtension_Decorated.getQuarkusProject(Unknown Source)
at io.quarkus.gradle.tasks.QuarkusAddExtension.addExtension(QuarkusAddExtension.java:59)
I figured something is lacking in the configuration, but I can't find what.
My build.gradle file:
plugins {
// Apply the application plugin to add support for building a CLI application in Java.
id 'application'
id 'io.quarkus'
}
repositories {
// Use JCenter for resolving dependencies.
jcenter()
mavenCentral()
gradlePluginPortal()
}
dependencies {
// Use JUnit test framework.
testImplementation 'junit:junit:4.13'
// This dependency is used by the application.
implementation group: 'io.quarkus', name: 'quarkus-resteasy', version: '1.12.2.Final'
implementation group: 'io.quarkus', name: 'quarkus-resteasy-mutiny', version: '1.12.2.Final'
}
application {
// Define the main class for the application.
mainClass = 'wells.App'
}
First of all, the --extensions parameter of the addExtension task does not take Maven coordinates but extension names without the quarkus- prefix, like so:
% ./gradlew addExtension --extensions="hibernate-validator"
> Task :addExtension
? Extension io.quarkus:quarkus-hibernate-validator has been installed
Additionally, I strongly recommend to let Quarkus manage the dependency versions for you. Quarkus is an opinionated framework and puts a lot of effort into managing dependencies for us. To take advantage of that, add an enforcedPlatform clause to your build.gradle script. Example:
dependencies {
implementation enforcedPlatform("io.quarkus:quarkus-universe-bom:1.12.2.Final")
// This dependency is used by the application.
implementation group: 'io.quarkus', name: 'quarkus-resteasy'
implementation group: 'io.quarkus', name: 'quarkus-resteasy-mutiny'
// Unit and integration tests
testImplementation 'io.quarkus:quarkus-junit5'
}
I've been working with Rails, PHP and Node.js and used to auto reloading after code changes.
Now I'm trying Java and came to two commands:
gradle build --continuous
gradle bootRun
But I see changes only after I restart gradle bootRun
Is it possible to rebuild and rerun spring after each code change?
Spring developer tools will handle this for you. Just add following dependency in your Gradle build file.
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
}
dependencies {
developmentOnly("org.springframework.boot:spring-boot-devtools")
}
For more details refer: https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html
HELP!
I'm using the built-in launch script with SpringBoot 1.3.6 and Gradle. Oh, and the distZip task to zip things up.
At one point not long ago, this was all working quite well... then I did -- I know not what -- to screw it up.
I've installed the package (unzipped the Zip, basically) on my raspberry Pi, and checked the ownership and permissions. Everything is owned by the user I want to run the app as (user "appservice" group "pi") and confirmed that the permissions for the files are -- if anything, too permissive (755 for the myapp/bin/myapp script and pretty much everything else).
I've put a symlink in /etc/init.d pointing to ~appservice/myapp/bin/myapp and I've run update-rc.d myapp defaults to get it into the system. Note the symlink itself is owned by root/root, but I believe it's supposed to be, isn't it?
I'm seeing two problems, that I think are interrelated.
First, no matter how I launch the script (on boot with init.d or manually with "sudo service myapp start"), it appears to run as root (specifically, paths that the app is trying to use to access files come out as /root/myapp/files instead of /home/appservice/myapp/files).
Second, the app will crash... specifically I get the syslog message "myapp.service start operation timed out. Terminating." followed by what looks like an orderly shutdown of the app. Oh, and related... if I launch with "sudo service myapp start" the command never returns. Which is new...
Third, the application log output is going to /var/log/syslog instead of to /var/log/myapp.log which seems counter to what the Spring Boot documentation says.
I'm in final regression testing of deployment for this, and (famous last words) I haven't changed anything recently... :) No, really, the most recent change relevant to distribution was adding the src/main/dist directory, and it was working after that (launching at boot as the correct user).
I'd post the launch script but AFAIK it's the default script provided by Spring Boot when you use the springBoot { executable = true } task.
Here's my full build.gradle file... I don't see anything amiss, but maybe you will.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'application'
apply from: 'gradle/gradle/helpers.gradle'
mainClassName = 'app.Application'
if (!hasProperty('mainClass')) {
ext.mainClass = 'app.Application'
}
springBoot {
executable = true
}
repositories {
mavenCentral()
}
sourceSets {
main {
java { srcDir 'src/main/java' }
resources { srcDir '/src/main/resources' }
}
test {
java { srcDir 'src/test/java' }
resources { srcDir 'src/test/resources' }
}
}
project.ext {
applicationVersion = "0.1.5-alpha"
applicationRelease = isApplicationRelease()
applicationDate = new Date()
applicationRevision = getRevision()
applicationVersionSnapshot = (!applicationRelease) ? "+SNAPSHOT.${asUTC(applicationDate, 'yyMMddHHmm')}.git${applicationRevision}" : ""
applicationVersionFull = "${applicationVersion}${applicationVersionSnapshot}"
}
jar {
baseName = 'myapp'
version = '0.9.0'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile group: 'com.sun.mail', name: 'javax.mail', version: '1.5.2'
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.3.6'
compile group: 'commons-cli', name:'commons-cli', version: '1.3.1'
compile group: 'org.json', name:'json', version: '20140107'
compile "commons-codec:commons-codec:1.10"
compile("org.springframework.boot:spring-boot-starter-hateoas")
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-mail")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework:spring-web")
compile("org.springframework:spring-messaging")
compile("joda-time:joda-time:2.2")
compile("com.fasterxml.jackson.core:jackson-databind")
compile("com.googlecode.json-simple:json-simple")
compile("org.jdom:jdom:2.0.0")
compile("org.hibernate:hibernate-validator")
compile("org.apache.tomcat.embed:tomcat-embed-el")
compile("org.apache.commons:commons-io:1.3.2")
compile("org.kamranzafar:jtar:2.3")
compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4")
compile("com.jcraft:jsch:0.1.53")
compile("javax.jmdns:jmdns:3.4.1")
testCompile("org.springframework.boot:spring-boot-starter-test")
}
task wrapper(type: Wrapper) {
gradleVersion = '2.14'
}
I would recommend to run and manage your application with systemd.
This makes it very easy to run the application under a specific user.
To do so, go on as follows:
First, create a service definition file /etc/systemd/system/myapp.service with this content:
[Unit]
Description=My App
[Service]
User=nobody
# The configuration file application.properties should be here:
WorkingDirectory=/home/appservice/myapp/files
ExecStart=/usr/bin/java -Xmx256m -jar myapp.jar
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target
Then, notify systemd of the new service file:
systemctl daemon-reload
and enable it, so it runs on boot:
systemctl enable myapp.service
At the end you can use the following commands to start/stop your new service:
systemctl start myapp
systemctl stop myapp
I'm restructuring/refactoring build process for a big(ish) project. Currently it contains over a dozen separate modules built with standalone build scripts each. I want to integrate them all into a single multiproject build in Gradle.
After I integrated all sources into a single tree, fixed build.gradles, I came upon the following problem. Dependencies for many modules contain something like:
dependencies {
compile group: 'com.company', name: 'Module', version: '1.2.3'
// ...
testCompile group: 'com.company', name: 'Module', version: '1.2.3', classifier: 'tests'
}
I want the build to use jars from the subproject, not from a repository. I replaced compile ... with compile project(':Module') and it works fine. However, I cannot find the way to pass 'tests' specifier to the testCompile project... dependency.
Is there a way to pick up the tests jar as a dependency to testCompile?
In the producing project you will need to declare the "Test" JAR as outgoing artifact.
configurations {
testUtils
}
task testUtilsJar(type: Jar) {
...
}
artifacts {
testUtils testUtilsJar
}
In the consuming project you depend on it as such:
dependencies {
testCompile project(path: ':Module', configuration: 'testUtils')
}
Looked around for this solution for much too long now, and I'm not sure if I missed it or just misstyped something, but my Gradle script will not compile. I am migrating to Gradle, and am very new with it. I am very used to using Maven for dependency management, but Gradle seems best me for now. From running this snippet of code:
dependencies {
compile group: 'org.bukkit', name: 'bukkit', version: '1.7.9-R0.1-SNAPSHOT'
compile('io.ibj:MattLib:1.1-SNAPSHOT') {
exclude group: 'de.bananaco'
exclude 'net.milkbowl:vault:1.2.27'
}
compile group: 'net.citizensnpcs', name: 'citizens', version: '2.0.12'
compile group: 'com.sk89q', name: 'worldedit', version: '5.6.1'
compile group: 'com.sk89q', name: 'worldguard', version: '5.9'
compile group: 'net.milkbowl', name: 'vault', version: '1.2.12'
compile fileTree(dir: 'libs', includes: ['*.jar'])
}
NOTE: I do have the java, maven, nexus, shadow, and rebel plugins applied.
When I run my Gradle task, I encounter this error:
Could not find method compile() for arguments [[io.ibj:MattLib:1.1-SNAPSHOT], build_1b5iofu9r9krp7o8mme0dqo9l$_run_closure2_closure8#66fb45e5] on root project 'project'
If I remove the MattLib dependency from my project and reinsert it as
compile 'io.ibj:MattLib:1.1-SNAPSHOT'
The script completes, but I have dependency issues. I read up here:
dependencies {
compile("org.gradle.test.excludes:api:1.0") {
exclude module: 'shared'
}
}
(From Chapter 50 From the Gradle Manual)
that what I have SHOULD work, but I am confused why it doesn't.
gradle --version output:
Groovy: 1.8.6
Ant: Apache Ant(TM) version 1.9.3 compiled on December 23 2013
Ivy: 2.2.0
JVM: 1.8.0_05 (Oracle Corporation 25.5-b02)
OS: Windows 7 6.1 amd64
Note that the compile, runtime, testCompile, and testRuntime configurations introduced by the Java plugin have been deprecated since Gradle 4.10 (Aug 27, 2018), and were finally removed in Gradle 7.0 (Apr 9, 2021).
The aforementioned configurations should be replaced by implementation, runtimeOnly, testImplementation, and testRuntimeOnly, respectively.
Make sure that you are editing the correct build.gradle file. I received this error when editing android/build.gradle rather than android/app/build.gradle.
compile is a configuration that is usually introduced by a plugin (most likely the java plugin) Have a look at the gradle userguide for details about configurations. For now adding the java plugin on top of your build script should do the trick:
apply plugin:'java'
It should be exclude module: 'net.milkbowl:vault:1.2.27'(add module:) as explained in documentation for DependencyHandler linked from here because ModuleDependency.exclude(java.util.Map) method is used.
In my case, all the compile statements has somehow arranged in a single line. separating them in individual lines has fixed the issue.
In my case the problem was mismatch in the gradle version. I have installed gradle on mac using
brew install gradle
and got the latest gradle which was 7.0
However when I cloned by project repo and executed the gradle taks it failed with below error
* What went wrong:
A problem occurred evaluating root project 'digital-engineering-course'.
> Could not find method compile() for arguments [org.springframework.boot:spring-boot-starter-web, build_bzpgd6h32w4m8umtmgs76ewog$_run_closure3$_closure8#b55ca3] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.
build.gradle file looked pretty normal to me as it has regular dependencies
dependencies {
compile("org.springframework.boot:spring-boot-starter-web") {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
compile("org.springframework.boot:spring-boot-starter-data-mongodb")
It took me a while to understand the problem is mismatch of version. Gradle is not able to find the method compile() because I was using gradle 7.0 in my bash.
And the project was supposed to be ran with gradle 4.8 (Actually gradle wrapper was to be used, but that was breaking for another interesting issue Could not find or load main class org.gradle.wrapper.GradleWrapperMain
(If interested please follow this for details)
The reason for failure is compile is that the compile, runtime, testCompile, and testRuntime configurations introduced by the Java plugin have been deprecated since Gradle 4.10, and were finally removed in Gradle 7.0.
So, to solve the problem I had to install the lower version of gradle. If you want to manage multiple version of gradle use sdkman (earlier known as gvm)
Installation on macOs / linux is as simple as executing below
curl -s "https://get.sdkman.io" | bash
Once done use
sdk list gradle
It will list out all the available versions of the gradle. As per your need install and use. for e.g
sdk install gradle 4.8 (this will choose the 4.8 by default in current shell)
sdk use gradle 4.8 (if already installed, this is suffice to switch between gradle version)
And now the build.gradle was able to compile and execute the task.
Add the dependency to your project-level build.gradle:
classpath 'com.google.gms:google-services:3.0.0'
Add the plugin to your app-level build.gradle:
apply plugin: 'com.google.gms.google-services'
app-level build.gradle:
dependencies {
compile 'com.google.android.gms:play-services-auth:9.8.0'
}
In my case I had to remove some files that were created by gradle at some point in my study to make things work. So, cleaning up after messing up and then it ran fine ...
If you experienced this issue in a git project, do git status and remove the unrevisioned files. (For me elasticsearch had a problem with plugins/analysis-icu).
Gradle Version : 5.1.1
Just for the record: I accidentally enabled Offline work under Preferences -> Build,Execution,Deployment -> Gradle -> uncheck Offline Work, but the error message was misleading