Following up on the first part of Java Enterprise 101, this time around we will get our hands dirty and implement the JEE stack with the help of Spring.
What you will need to follow along:
- A working installation of JDK 10 (or higher)
- A working internet connection
- Optionally, your favorite editor or IDE
Setting up the project
We start... well, at the start. At start.spring.io to be precise. This URL will lead you to the Spring Initializr - a handy little website where you can mix and match libraries and packages to suit your project. In our case, we want to have the Web, JPA and H2 packages:
I also changed the project name from "com.example" to "org.example" as well as the project type from Maven to Gradle. You can also use Maven perfectly fine, but for this guide I will be using Gradle. Once you hit the "generate project" button, you will end up with a *.zip file. Unpack it to a directory of your choice; this will become our project directory. Inside, you will find:
- build.gradle: this file contains your build configuration.
- gradlew / gradlew.bat: Gradle Wrapper. Always use these, never "gradle" itself.
- settings.gradle: basically your Gradle module structure. We won't touch this, at least for now.
- .gitignore: a very basic configuration to exclude binaries and generated files.
- src: the source directory for your code. Follows the maven project structure.
Let's have a look at the generated build.gradle script, shall we?
buildscript {
ext {
springBootVersion = '2.0.2.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'org.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
As far as buildscripts go, this is actually quite readable. It first says that it uses the spring-boot-gradle-plugin
and the Maven Central repository for fetching its dependencies. Not really surprising, is it?
In the next section we have a list of gradle plugins which are applied to our project. Feel free to replace the eclipse
plugin with the plugin for your IDE instead (e.g. idea
for IntelliJ users). If you stick to editor and command line, you don't need this at all.
In my version, I changed the sourceCompatibility
to 1.10
so I can use the var
keyword from Java 10. The build script also mentions the maven coordinates of our project. Be a good citizen and always stick to semantic versioning (SemVer)! In the final section, we see a list of our dependencies, both for compiling and for testing our project.
/!\ IMPORTANT:
In order to make this buildscript work properly with Java 9 and higher (modularized Java platform libs) you also need to add the following dependency:
compile('javax.xml.bind:jaxb-api')
We are not going to actually use JAXB, but you will get a
NoClassDefFoundError
if you are missing this dependency. Hopefully, the Spring Boot Team will add it to their starter packs in the near future.
The Spring Boot Starter packages are "meta-packages" which contain an opinionated selection of libraries for common tasks. For example:
-
spring-boot-starter-web
contains an (embedded)Tomcat
as an application container andJackson
for JSON handling -
spring-bot-stater-data-jpa
containsHibernate
as the JPA provider -
spring-boot-starter-test
containsJUnit
That's nice, but what if you don't agree with the choices of the Spring Boot team? Don't worry, they've got you covered. If Spring Boot detects the presence of another library, say TestNG, this library will automatically take precedence over the default. In many cases, you don't even have to configure anything manually to make this happen; just add the dependency in gradle and rebuild the project. If you want some more exotic alternatives that Spring doesn't recognize, you will have to do a little bit more work, but that's for another article. For now, let's make use of what we get out of the box.
Getting to work!
First of all, you should verify that your setup is working as intended. The best way to do so is to open a command prompt (yes, even windows cmd
will do...) and navigate to your project folder (where build.gradle
is located). Run the following command:
gradlew build
Under windows cmd
, you might need to use gradlew.bat
instead. Anyways, this will:
- download all required dependencies from maven central
- compile your source files
- execute all your tests
"But we ain't got no tests!" you say? Well, Spring Initializr always includes one particular test case that may seem a little odd at first sight, but actually makes a lot of sense. Open up the file DemoApplicationTests.java
(if you called your app something other than Demo
, replace it accordingly).
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Test
public void contextLoads() {
}
}
This test does exactly what the name implies: it asserts that the Spring context loads up correctly. If this basic test fails, it means that you have a severe issue with your Spring / Spring Boot Setup that needs to be sorted out before continuing on. We should be okay with our setup now.
The other class that is generated by the Spring Initializr for you is the DemoApplication.java
class:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
This is your main class, the entry point into your server-side application.
/!\ IMPORTANT:
The package in which this class resides is considered to be the root package by Spring Boot. Make sure that all of your source code either resides in the root package, or one of its (direct or indirect) child packages, otherwise Spring Boot will not be able to locate your classes.
Our first Spring REST Endpoint
Ready to write some actual code? Create a Java file and place it in your root package (or a sub-package). I place mine in org.example.demo.rest:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldRestController {
@RequestMapping(method = RequestMethod.GET, path = "/api/hello-world")
@ResponseBody
public String sayHello(){
return "Hello World!";
}
}
There you go. That's all it takes to build a REST endpoint with Spring Boot. The @RestController
annotation tells Spring Boot what to do with this class (later on in this tutorial series we wil see other examples as well). The method sayHello
(the name does not matter in the slightest, as long as it is a valid Java identifier you can name it whatever you like) implements a REST endpoint. The @RequestMapping
provides the information to Spring which incoming requests to route to this endpoint. In this case, we bind it to the resource path /api/hello-world
and to the request method GET
. With @ResponseBody
, we tell Spring that this method produces a result that should be returned to the client as the body of the HTML response.
Booting it up
Now for the really awesome part. Go to your command line interface again and navigate to your project folder (if you haven't done so already). Run the gradlew build
command once more. Upon success, navigate to the build/libs sub-directory. In there, you will find a single .jar file. Run this file with java
as you normally would:
java -jar demo-0.0.1-SNAPSHOT.jar
This will start up your application server for you. Wait, what? But we did not even install a Tomcat yet! The funny thing is: you don't need to. While you can deploy your Java Enterprise application now in the "traditional" way as a war file, you can also run it as a regular java application, because the application container is alreay embedded in your application. Once it has finished starting up, open up a web browser and navigate to:
http://localhost:8080/api/hello-world
You should see our "Hello World!" message showing up.
Testing
We wouldn't be much of a developer in 2018 if we didn't write proper tests, now would we? Luckily, Spring makes it relatively easy for us to test our REST endpoints. However, some setup is required. I will expand our existing test class for simplicities sake (and omit the existing test case in the following listings). Here's the setup code we need:
package org.example.demo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup(){
this.mvc = MockMvcBuilders
.webAppContextSetup(context)
.build();
}
}
This helps us to set up the MockMVC helper. There is a lot more to this class than it initially appears to be; we will explore in another tutorial why exactly this class is so awesome for testing Spring REST endpoints. For now, just consider it a nifty helper to formulate a REST request. Note that the set up code above can be moved to a testing base class; it's never going to change, and there is no need to repeat it in every test class. Let's have a look at the code in more detail.
@Autowired
is the dependency injection annotation used by Spring, very similar to @Inject
used by JSR 330 implementations (e.g. Google Guice). With this, we basically tell Spring to pass in the single WebApplicationContext
instance to our test. We then use JUnit's @Before
method to set up our MockMvc
instance using our web context. That is how the MockMvc
instance "finds" your endpoints.
Now, let's write an actual test for our REST endpoint:
@Test
public void testHelloWorldEndpoint() throws Exception {
var response = this.mvc.perform(
get("/api/hello-world")
).andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
assertEquals("Hello World!", response);
}
This internal DSL allows us to perform
the get
request and we expect
the HTTP status
code to be ok
, and we return
the response
content
as a String. This reads like prose. Compiler-checked prose that reduces dozens of lines with assertions into a single line. If that isn't beautiful, I don't know what is.
Run this test and watch it pass! You can either do it in the command line with
gradlew test
... (this runs all tests) or directly in your IDE.
Closing Thoughts
This has become quite a long article - a lot longer than I intended. However, if we consider how many lines of actual code we actually wrote (about 10?) it is hard to argue that Java is "such a verbose language", considering what we achieved in terms of functionality. We also have the foundations in place to extend our server with a data model and a database connection in the next article. We literally merely scratched the surface. Brevity aside, the true advantages of Spring Boot and the JEE architecture are not yet visible. Bear with me for now please ;)
Top comments (6)
I am trying this out but get a compile error at the first build
gradlew.bat build
FAILURE: Build failed with an exception.
What went wrong:
Execution failed for task ':compileJava'.
Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
Get more help at help.gradle.org
BUILD FAILED in 2s
1 actionable task: 1 executed
Any ideas??
So usually this error means that gradle cannot find your Java Compiler, either because it is not installed at all or your PATH variable (windows) or JAVA_HOME (windows + linux) is not set up properly.
Try running
javac -version
in the same shell you are running your gradle command in to see what version of the Java Compiler is being called. If that command produces an error, you know that the JDK is not set up properly or not properly registered with the OS. On windows, it usually suffices to add the path to the JDK installation to the PATH environment variable. As for Linux and Mac, sorry I don't know; but Google / Stackoverflow should hold the answer for that.Hmmm.. doesnt seem to be the problem.
It finds javac 10.0.1
Thanks for the quick reply by the way!!
Ok then it is very likely the
JAVA_HOME
environment variable that is not set up. Gradle is unfortunately rather picky when it comes to the setup and the environment variables. Here's a tutorial on how to set it up on windows (as you use the *.bat file I assume that is what you need):mkyong.com/java/how-to-set-java_ho...
If that still doesn't work, feel free to try Maven instead; for this tutorial it really does not matter.
Java Enterprise 101??? Spring is not Java Enterprise, it's another stack
JEE is technically another stack, yes. But the basuc patterns and principles are the same, even the terms are interchangeable. When I say "Java Enterprise" I mean "Java as it is being used in Enterprise Applications". I did this distinction in the previous article (which is linked in the beginning of this article), I felt that repeting it wouldn't benefit anyone. Apparently I was wrong.