This is the documentation for my course titled "Getting Started with Spring Boot 2". This documentation will be a central repository for a lot of the course details.
Course Details
If you’re reading this document than you have most likely signed up for my course "Getting Started with Spring Boot 2". I want to thank you for supporting me and taking the time to invest in yourself. I hope you enjoy this course and if you have any questions please feel free to contact me.
About
This course offers hands-on experience building Spring Framework applications using Spring Boot. The first thing that is going to stand out is that we are going to move away from the boring, non-useful demos. In the new course, we are going to build a practical application from start to finish. This will allow you to see the process I use for building applications from idea to launch.
One of the big thing I heard from my students in my first course is that they wanted more information on how to go into production. A great application isn’t that useful if we can deploy it into a production environment. We will take a look at using one of the most popular cloud services around, Amazon Web Services (AWS). On completion, students will have the foundation they need to build enterprise-ready Java applications.
Prerequisites
-
Java Developer
-
Web Development
-
Git & Github
-
Heard of Spring Framework & Spring Boot
-
You want to see some of the new features in:
-
Spring Framework 5
-
Spring Boot 2.0
-
Requirements
-
Java 9
-
Mac OS / Windows / Linux
Curriculum
-
Introduction
-
Course Introduction
-
Goals for this course
-
Development Environment Setup
-
Course Documentation
-
Spring Framework vs Spring Boot
-
-
Project Overview
-
Introduction
-
Create the Initial Project
-
Running with Spring Boot
-
Requirements
-
UI Intro
-
UI Templates
-
Domain Model
-
-
Github
-
Introduction
-
Create the initial repository
-
README setup
-
Branching strategy
-
-
Spring Boot Essentials
-
Introduction
-
Spring Boot devtools
-
Configuration & Properties
-
Profiles
-
Debugging & Logging
-
Actuator
-
-
Spring MVC: Model
-
Spring Data JPA Introduction
-
Link Entity & Repository
-
Project Lombok Refactor
-
Comment & Vote Entities
-
Auditing Aware
-
-
Database Layer
-
Properties
-
H2 in memory database setup
-
Creating data using SQL
-
Creating data using Java
-
Course Project
Course Assets
If we need to provide a download this would be a great place to link to those assets.
Development Environment
Java
SDKMan
Build Tools
Maven
Gradle
IDE & Text Editors
IntelliJ
Spring Tool Suite (Eclipse)
Visual Studio Code
Amazon Web Services (AWS)
What’s new in Spring
Spring Framework 5
JDK 8+ and Java EE 7+ Baseline
-
Entire framework codebase based on Java 8 source code level now.
-
Improved readability through inferred generics, lambdas, etc.
-
Conditional support for Java 8 features now in straight code.
-
-
Full compatibility with JDK 9 for development and deployment.
-
On classpath as well as module path (with stable automatic module names).
-
Framework build and test suite passes on JDK 9 (runs on JDK 8 by default).
-
-
Java EE 7 API level required in Spring’s corresponding features now.
-
Servlet 3.1, Bean Validation 1.1, JPA 2.1, JMS 2.0
-
Recent servers: e.g. Tomcat 8.5+, Jetty 9.4+, WildFly 10+
-
-
Compatibility with Java EE 8 API level at runtime.
-
Servlet 4.0, Bean Validation 2.0, JPA 2.2, JSON Binding API 1.0
-
Tested against Tomcat 9.0, Hibernate Validator 6.0, Apache Johnzon 1.1
-
Removed Packages, Classes and Methods
-
Package beans.factory.access (BeanFactoryLocator mechanism).
-
Package jdbc.support.nativejdbc (NativeJdbcExtractor mechanism).
-
Package mock.staticmock removed from spring-aspects module.
-
No support for AnnotationDrivenStaticEntityMockingControl anymore.
-
-
Packages web.view.tiles2 and orm.hibernate3/hibernate4 dropped.
-
Minimum requirement: Tiles 3 and Hibernate 5 now.
-
-
Dropped support: Portlet, Velocity, JasperReports, XMLBeans, JDO, Guava.
-
Recommendation: Stay on Spring Framework 4.3.x for those if needed.
-
-
Many deprecated classes and methods removed across the codebase.
-
A few compromises made for commonly used methods in the ecosystem.
-
General Core Revision
-
JDK 8+ enhancements:
-
Efficient method parameter access based on Java 8 reflection enhancements.
-
Selective declarations of Java 8 default methods in core Spring interfaces.
-
Consistent use of JDK 7 Charset and StandardCharsets enhancements.
-
-
JDK 9 compatibility:
-
Avoiding JDK APIs which are deprecated in JDK 9 wherever possible.
-
Consistent instantiation via constructors (with revised exception handling).
-
Defensive use of reflection against core JDK classes.
-
-
Non-null API declaration at the package level:
-
Nullable arguments, fields and return values explicitly annotated with @Nullable.
-
Primarily for use with IntelliJ IDEA and Kotlin, but also Eclipse and FindBugs.
-
Some Spring APIs are not tolerating null values anymore (e.g. in StringUtils).
-
-
Resource abstraction provides isFile indicator for defensive getFile access.
-
Also features NIO-based readableChannel accessor in the Resource interface.
-
File system access via NIO.2 streams (no FileInput/OutputStream used anymore).
-
-
Spring Framework 5.0 comes with its own Commons Logging bridge out of the box:
-
spring-jcl instead of standard Commons Logging; still excludable/overridable.
-
Autodetecting Log4j 2.x, SLF4J, JUL (java.util.logging) without any extra bridges.
-
-
spring-core comes with ASM 6.0 (next to CGLIB 3.2.5 and Objenesis 2.6).
Core Container
-
Support for any @Nullable annotations as indicators for optional injection points.
-
Functional style on GenericApplicationContext/AnnotationConfigApplicationContext
-
Supplier-based bean registration API with bean definition customizer callbacks.
-
-
Consistent detection of transaction, caching, async annotations on interface methods.
-
In case of CGLIB proxies.
-
-
XML configuration namespaces streamlined towards unversioned schemas.
-
Always resolved against latest xsd files; no support for deprecated features.
-
Version-specific declarations still supported but validated against latest schema.
-
-
Support for candidate component index (as alternative to classpath scanning).
Spring Web MVC
-
Full Servlet 3.1 signature support in Spring-provided Filter implementations.
-
Support for Servlet 4.0 PushBuilder argument in Spring MVC controller methods.
-
MaxUploadSizeExceededException for Servlet 3.0 multipart parsing on common servers.
-
Unified support for common media types through MediaTypeFactory delegate.
-
Superseding use of the Java Activation Framework.
-
-
Data binding with immutable objects (Kotlin / Lombok / @ConstructorProperties)
-
Support for the JSON Binding API (with Eclipse Yasson or Apache Johnzon as an alternative to Jackson and GSON).
-
Support for Jackson 2.9.
-
Support for Protobuf 3.
-
Support for Reactor 3.1 Flux and Mono as well as RxJava 1.3 and 2.1 as return values from Spring MVC controller methods targeting use of the new reactive WebClient (see below) or Spring Data Reactive repositories in Spring MVC controllers.
-
New ParsingPathMatcher alternative to AntPathMatcher with more efficient parsing and extended syntax.
-
@ExceptionHandler methods allow RedirectAttributes arguments (and therefore flash attributes).
-
Support for ResponseStatusException as a programmatic alternative to @ResponseStatus.
-
Support script engines that do not implement Invocable via direct rendering of the script provided using ScriptEngine#eval(String, Bindings), and also i18n and nested templates in ScriptTemplateView via the new RenderingContext parameter.
-
Spring’s FreeMarker macros (spring.ftl) use HTML output formatting now (requiring FreeMarker 2.3.24+).
Spring WebFlux
-
New spring-webflux module, an alternative to spring-webmvc built on a reactive foundation — fully asynchronous and non-blocking, intended for use in an event-loop execution model vs traditional large thread pool with thread-per-request execution model.
-
Reactive infrastructure in spring-core such as Encoder and Decoder for encoding and decoding streams of Objects; DataBuffer abstraction, e.g. for using Java ByteBuffer or Netty ByteBuf; ReactiveAdapterRegistry for transparent support of reactive libraries in controller method signatures.
-
Reactive infrastructure in spring-web including HttpMessageReader and HttpMessageWriter that build on and delegate to Encoder and Decoder; server HttpHandler with adapters to (non-blocking) runtimes such as Servlet 3.1+ containers, Netty, and Undertow; WebFilter, WebHandler and other non-blocking contract alternatives to Servlet API equivalents.
-
@Controller style, annotation-based, programming model, similar to Spring MVC, but supported in WebFlux, running on a reactive stack, e.g. capable of supporting reactive types as controller method arguments, never blocking on I/O, respecting backpressure all the way to the HTTP socket, and running on extra, non-Servlet containers such as Netty and Undertow.
-
New functional programming model ("WebFlux.fn") as an alternative to the @Controller, annotation-based, programming model — minimal and transparent with an endpoint routing API, running on the same reactive stack and WebFlux infrastructure.
-
New WebClient with a functional and reactive API for HTTP calls, comparable to the RestTemplate but through a fluent API and also excelling in non-blocking and streaming scenarios based on WebFlux infrastructure; in 5.0 the AsyncRestTemplate is deprecated in favor of the WebClient.
Kotlin support
-
Null-safe API when using Kotlin 1.1.50 or higher.
-
Support for Kotlin immutable classes with optional parameters and default values.
-
Functional bean definition Kotlin DSL.
-
Functional routing Kotlin DSL for WebFlux.
-
Leveraging Kotlin reified type parameters to avoid specifying explicitly the Class to use for serialization/deserialization in various APIs like RestTemplate or WebFlux APIs.
-
Kotlin null-safety support for @Autowired/@Inject and @RequestParam/@RequestHeader/etc annotations in order to determine if an injection point or handler method parameter is required or not.
-
Kotlin script support in ScriptTemplateView for both Spring MVC and Spring WebFlux.
-
Array-like setters added to Model, ModelMap and Environment.
-
Support for Kotlin autowired constructor with optional parameters.
-
Kotlin reflection is used to determine interface method parameters.
Testing Improvements
-
Complete support for JUnit 5’s Jupiter programming and extension models in the Spring TestContext Framework.
-
SpringExtension: an implementation of multiple extension APIs from JUnit Jupiter that provides full support for the existing feature set of the Spring TestContext Framework. This support is enabled via @ExtendWith(SpringExtension.class).
-
@SpringJUnitConfig: a composed annotation that combines @ExtendWith(SpringExtension.class) from JUnit Jupiter with @ContextConfiguration from the Spring TestContext Framework.
-
@SpringJUnitWebConfig: a composed annotation that combines @ExtendWith(SpringExtension.class) from JUnit Jupiter with @ContextConfiguration and @WebAppConfiguration from the Spring TestContext Framework.
-
@EnabledIf: signals that the annotated test class or test method is enabled if the supplied SpEL expression or property placeholder evaluates to true.
-
@DisabledIf: signals that the annotated test class or test method is disabled if the supplied SpEL expression or property placeholder evaluates to true.
-
-
Support for parallel test execution in the Spring TestContext Framework.
-
New before and after test execution callbacks in the Spring TestContext Framework with support for TestNG, JUnit 5, and JUnit 4 via the SpringRunner (but not via JUnit 4 rules).
-
New beforeTestExecution() and afterTestExecution() callbacks in the TestExecutionListener API and TestContextManager.
-
-
MockHttpServletRequest now has getContentAsByteArray() and getContentAsString() methods for accessing the content (i.e., request body).
-
The print() and log() methods in Spring MVC Test now print the request body if the character encoding has been set in the mock request.
-
The redirectedUrl() and forwardedUrl() methods in Spring MVC Test now support URI templates with variable expansion.
-
XMLUnit support upgraded to 2.3.
This information came straight from the Spring Framework Wiki.
Spring Boot 2
Spring Boot History
Before we dive into what’s new I want to take this chance to take a quick look at the history of Spring Boot. In a blog post published in August 2013, Phil Webb announced the first milestone release of a new project called Spring Boot.
Spring Boot aims to make it easy to create Spring-powered, production-grade applications and services with minimum fuss. It takes an opinionated view of the Spring platform so that new and existing users can quickly get to the bits they need. You can use it to create stand-alone Java applications that can be started using ‘java -jar’ or more traditional WAR deployments.
About 9 months later in April 2014 Spring Boot 1.0 was released. Since then there have been numerous minor releases that have brought us some really great features.
-
Spring Boot 1.1 (June 2014)
-
spring-boot-starter-test
-
Metrics & Health Endpoints
-
Elastic Search, Apache Solr, Spring Social & Spring Integration Auto-configuration
-
Additional Templating support (Adding Freemaker, Groov, and Velocity)
-
-
Spring Boot 1.2 (March 2015)
-
Servlet 3.1, Tomcat 8 & Jetty 9
-
Spring 4.1
-
@SpringBootApplication Annotation
-
Email Support
-
-
Spring Boot 1.3 (December 2016)
-
Upgrade to Spring Framework 4.2
-
Upgrade to Spring Security 4.0
-
Developer Tools
-
Caching Auto-configuration
-
Fully executable JARs and service support
-
-
Spring Boot 1.4 (January 2017)
-
Spring 4.3
-
Hibernate 5
-
Testing Improvements
-
Integration Starter
-
Couchbase and Neo4J Support
-
-
Spring Boot 1.5 (February 2017)
-
Loggers endpoint
-
Apache Kafka support
-
Cloud Foundry actuator extensions
-
LDAP support
-
Testing updates
-
What’s new in Spring
What’s new in Spring Boot 2? If we are going to talk about the biggest feature we need to start with the upgrade to Spring Framework 5. While Spring Framework 5 has been out since September 2017, most developers (like me) have been waiting for the release of Spring Boot 2. Spring Framework 5 introduces a long list of new features but I just want to touch on a few of the major ones here.
What’s new in Spring Framework 5
Java 8+ Baseline
If you wish to build Spring Framework applications going forward you must be on a minimum of Java 8. Your first thought might be this is a great change for all of us but in fact its an even bigger one for the Spring team. This gave them a chance to make some major changes to update the code base to all the new features in Java 8 like Lambdas and streams. This not only ads readability throughout the code but also offers some performance improvements to the core of the framework.
Java 9 Support
If you want to use Java 9 you will need to update to Spring Framework 5 and therefore need to update to Spring Boot 2. I know many of us might not be on the latest and greatest version of Java in production but this is a great chance for you to play around with all the cool new toys. This works pretty seamlessly out of the box using the standard classpath but I have read a few people having issues moving to Java 9 modules.
Spring MVC
While Spring MVC isn’t the main character in this story there were some nice upgrades. I won’t go through them all here so head over to documentation for Spring Framework 5.
Spring Webflux
The reactive stack is our main character in the story of Spring Framework 5. This is a different way of thinking but fortunately for us, we don’t have to learn a whole new way of writing applications. Spring WebFlux is a fully asynchronous and non-blocking web framework built from the ground up allowing us to handle a massive number of concurrent connections. While this is a complete paradigm shift it is really easy to get started.
Kotlin Support
Kotlin support was added to http://start.spring.io but in Spring Framework 5 there is dedicated support for the language. With the dedicated support came some nice features that you can read about here.
Testing Improvements
The biggest change in the testing landscape is the complete support for JUnit 5’s Jupiter programming and extension model. I will mention this again later but when you fire up a new Spring Boot 2 application you are still using JUnit 4 by default however it is a simple change if you want to start using JUnit 5.
What’s new in Spring Boot 2
Third-party Library Upgrades
With any new release of Spring Boot, the Spring team has an opportunity to update various dependencies.
-
Thymeleaf 3
-
Jetty 9.4
-
Tomcat 8.5
-
Hibernate 5.2
-
Flyway 5
-
Gradle 4
Reactive Spring Data & Spring Security
With the move to Spring WebFlux, Spring Data also provides support for reactive applications. Currently, Cassandra, MongoDB, Couchbase and Redis all have reactive API support. Spring Boot includes starter-POMs for all of these which should make getting started really easy.
We also gain the ability to use Spring Security 5.0 in our reactive applications. When Spring Security is on the classpath auto-configuration is provided for WebFlux applications.
Actuator
Spring Boot Actuator is nothing new but it has been rewritten from the ground up. If you’re not familiar with the Actuator project it automatically exposes endpoints to get information about that status of your application. The Actuator in Spring Boot 1.x was written against the servlet API and with the new reactive stack, the Spring team needed a solution that would work with both. Along with that, there were many changes to Actuator:
-
Redesign for both servlet & reactive
-
Status & health (all the details) were separated out
-
Simplified Security model
-
Move to micrometer (think SLF4J but for metrics)
-
Improved JSON Structures
-
Simplified process for creating User-Defined Endpoints.
-
@Endpoint
-
@WebEndpoint
-
@JmxEndpoint
-
This might be an area that folks have some trouble upgrading because of the security model changes which we will talk about those in a few. By default, all web endpoints are available beneath the path /actuator with URLs of the form /actuator/{id}. The /actuator base path can be configured by using the management.endpoints.web.base-path property.
There is a dedicated detailed document for Spring Boot Actuator Web API Endpoints and this is a great place to start.
Gradle Plugin
I have always been a big fan of Gradle and I am really excited to see that the team took this opportunity to rewrite the Gradle plugin.
The Spring Boot Gradle Plugin provides Spring Boot support in Gradle, allowing you to package executable jar or war archives, run Spring Boot applications, and use the dependency management provided by spring-boot-dependencies. Spring Boot’s Gradle plugin requires Gradle 4.0 or later.
images::pexels-photo-209728.jpeg[]
Getting Started
To get started with the plugin it needs to be applied to your project.
buildscript {
repositories {
maven { url 'https://repo.spring.io/libs-milestone' }
}
dependencies {
classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RC1'
}
}
apply plugin: 'org.springframework.boot'
Building executable jars and wars
The bootRepackage task has been replaced with bootJar and bootWar tasks for building executable jars and wars respectively. Both tasks extend their equivalent standard Gradle jar or war task, giving you access to all of the usual configuration options and behavior.
Dependency management
Spring Boot’s Gradle plugin no longer automatically applies the dependency management plugin. Instead, Spring Boot’s plugin now reacts to the dependency management plugin being applied by importing the correct version of the spring-boot-dependencies BOM. This gives you more control over how and when dependency management is configured. For most applications applying the dependency management plugin will be sufficient:
apply plugin: 'io.spring.dependency-management'
The Gradle Plugin also has its own documentation and there is some really great information in there. Anyone that wants to get started using Gradle should start by checking out the link below.
Simplified Security
In Spring Boot 2.x, one of the main goals was to simplify security configuration and make adding custom security easy. By default, everything is secured, including static resources and Actuator endpoints. If Spring Security is on the classpath, Spring Boot will add @EnableWebSecurity and rely on Spring Security’s content-negoation to decide which authentication mechanism to use.
Once users decide that they want to add custom security, the default security configuration provided by Spring Boot will back off completely. At this point, users need to explicitly define all the bits they want to secure. This means security configuration is now in one place and avoids any kind of ordering issues with existing WebSecurityConfigurerAdapters.
Here is an example of a custom security:
http
.authorizeRequests()
// 1
.requestMatchers(EndpointRequest.to("status", "info"))
.permitAll()
// 2
.requestMatchers(EndpointRequest.toAnyEndpoint())
.hasRole("ACTUATOR")
// 3
.requestMatchers(StaticResourceRequest.toCommonLocations())
.permitAll()
// 4
.antMatchers("/**")
.hasRole("USER")
.and()
... // additional configuration
-
/status and /info endpoints do not require authentication.
-
All other actuator endpoints are protected by the ACTUATOR role.
-
Common static resource locations are open to all.
-
All other application endpoints are protected by the USER role.
Actuator Security
With the actuator endpoints locked down, you will need to include or exclude the endpoints you want to see. These are the properties that now control that and have changed since 1.x
# ENDPOINTS WEB CONFIGURATION (WebEndpointProperties)
management.endpoints.web.exposure.include=info,health # Endpoint IDs that should be included or '*' for all.
management.endpoints.web.exposure.exclude= # Endpoint IDs that should be excluded.
management.endpoints.web.base-path=/actuator # Base path for Web endpoints. Relative to server.servlet.context-path or management.server.servlet.context-path if management.server.port is configured.
management.endpoints.web.path-mapping= # Mapping between endpoint IDs and the path that should expose them.
If you're ever unsure what properties to use bookmark the common application properties documentation.
HTTP/2 Support
It’s hard to believe but the HTTP 1.1 specification was released back in 1996. I think this comes without saying but the web is very different today. If you want to enable HTTP/2 in your Spring MVC or WebFlux applications you can use the following property.
server.http2.enabled=true
This support depends on the chosen web server and the application environment since that protocol is not supported out-of-the-box by JDK8. Please check out the documentation for more details.
Configuration Properties
In Spring Boot 1.x this notion of relaxed binding was supported and all that meant is there were multiple ways you could create a property name (camel case, underscore, hyphen) and that property would all bind to the same property.
This still works the same but what they did tighten up was the way that you read variables in your own code. The @Value annotation is a core container feature, and it does not provide the same features as type-safe configuration properties.
Metrics
Spring Boot’s own metrics have been replaced by Micrometer. This is being developed by Pivotal and quickly being adopted across projects within Pivotal.
Spring Boot Actuator provides dependency management and auto-configuration for Micrometer, an application metrics facade that supports numerous monitoring systems, including:
-
Atlas
-
Datadog
-
Ganglia
-
Graphite
-
Influx
-
JMX
-
New Relic
-
Prometheus
-
SignalFx
-
Simple (in-memory)
-
StatsD
-
Wavefront
To learn more about Micrometer visit https://micrometer.io/
This might cause a little bit of an upgrade pain for those doing a ton of logging of custom metrics.
Quartz Scheduler
Spring Boot 2 provides support for the Quartz scheduler that can be used via the spring-boot-starter-quartz dedicated starter. Both in-memory and JDBC stores can be configured.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
HikariCP Connection Pool
The default connection pool has switched from Tomcat to HikariCP. If you used spring.datasource.type to force the use of Hikari in a Tomcat-based application, you can now remove that override. Similarly, if you want to stay with the Tomcat connection pool, simply add the following to your configuration:
spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource
Developer Tools
By default, each time your application restarts, a report showing the condition evaluation delta is logged. The report shows the changes to your application’s auto-configuration as you make changes such as adding or removing beans and setting configuration properties.
To disable the logging of the report, set the following property:
spring.devtools.restart.log-condition-evaluation-delta=false
Kotlin Support
We talked earlier in this article about the official support for Kotlin. There is also a dedicated portion of the documentation to Kotlin.
JUnit 5
As I said earlier the default for a Spring Boot application is to still use JUnit 4. If you want to switch to JUnit 5 you will need to exclude JUnit 4 from the spring boot starter test and add the required dependencies. You will also need to add the plugin listed below.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>${junit-platform.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
Spring Boot 2 Migration Guide
It probably goes without saying but with any major release like this simply flipping the version number in production isn’t your best upgrade path. The first thing I would do is read through the Spring Boot 2.0 Migration Guide. For me I found most of my issues revolved around the simplified security model changes and property changes. The migration guide includes an awesome tip for migrating your property files.
With Spring Boot 2.0, many configuration properties were renamed/removed and developers need to update their application.properties/application.yml accordingly. To help you with that, Spring Boot ships a new spring-boot-properties-migrator module. Once added as a dependency to your project, this will not only analyze your application’s environment and print diagnostics at startup, but also temporarily migrate properties at runtime for you. This is a must have during your application migration:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
</dependency>
I am not sure what your thoughts are on this but I would certainly start playing with Spring Boot 2 and working on migrating your code over to it but with any major release, I usually wait until the next point release. This isn’t just for Spring, I do this with pretty much everything from Apple to Pivotal to Angry Birds!
This document was created from this blog post. You can also watch a video of me talking about this post here.
Github
Repositories
Main Repo
This is a reddit clone built using Spring Boot 2. I created this project to show off all the cool features of Spring Boot 2.
UI Templates
These are the HTML templates that we are using to build out UI for this course. I wanted to provide the raw templates before we start introducing Thymleaf into the equation.
Learning
This is a collection of websites, training videos and resources to help you learn Git & Github. While this course isn’t specifically about Git or Github I think it is an important skill to learn. If you have some time please consider going through some of the links below.
Websites
Resources
Using Git & Github
If you’re brand new to Git & Github please don’t let that throw you off of this course. I have left you with some resources to learn these tools but they are not a requirement for you to learn about what is new in Spring Boot 2. I also don’t want you to get discouraged if you are not an IntelliJ user. There is nothing I showed in this section that you couldn’t do from your IDE or editor of choice. I just happen to be using IntelliJ so I wanted to show off the tools available within the IDE.
Spring Boot Essentials
Actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# open up all of the endpoints for demo purposes
# only expose certain endpoints ["health","info"]
management.endpoints.web.expose=*
management.endpoints.web.exclude=env
management.endpoint.health.show-details=true
Building Springit
This section of the documentation is going to contain details about each section of the course as we begin to build out our Reddit clone. In each of these sections I will provide information, code or even links to topics that we discuss as we start to develop our application. Each section underneath Building Springit represents a section in the course from here on out. So if you’re working on the Service Layer section, click on the Service Layer sub section here to find all of the accompanying documentation.
| MVC stands for Model View Controller |
Spring MVC: Model
In the project overview section we discussed the domain model. For reference I have included our basic model diagram below. Please remember that this is probably going to change as we begin to build out our application but this should give us what we need to get started.
Now that we know what we need to build, how are we going to build it?
JPA
The Java Persistence API (JPA) is a Java application programming interface specification that describes the management of relational data in applications using Java Platform, Standard Edition and Java Platform, Enterprise Edition.
Persistence in this context covers three areas:
-
The API itself, defined in the javax.persistence package
-
The Java Persistence Query Language (JPQL)
-
object/relational metadata
|
Class ⇒ Database Table |
JPA is just the specification and by itself isn’t all that usefull. What you need is a JPA provider that implements the specification. A couple of the most popular implementations are:
-
Hibernate
-
Eclipse Link
Spring Data
Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store.
Spring Data JPA
Implementing a data access layer of an application has been cumbersome for quite a while. Too much boilerplate code has to be written to execute simple queries as well as perform pagination, and auditing. Spring Data JPA aims to significantly improve the implementation of data access layers by reducing the effort to the amount that’s actually needed. As a developer you write your repository interfaces, including custom finder methods, and Spring will provide the implementation automatically.
| Spring Data JPA uses Hibernate under the hood. Through a Spring Boot Starter and Auto Configuration you don’t have to do much to get Hibernate setup. This is huge because this is one of the things that deters people from starting with Hibernate in the first place. |
Spring Data JPA Dependencies
You can select Spring Data JPA as one of the options using the Spring Initializr or by adding the following dependency to your pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
| What dependencies does the spring-boot-starter-data-jpa include? |
Entities
Now that we know a little bit about Spring Data it’s time we start to create our entities. Before we do so this is a good time to create a new branch to work on our new feature.
git checkout -b spring-mvc-model
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Link {
@Id
@GeneratedValue
private Long id;
private String title;
private String url;
// comments
}
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Comment {
@Id
@GeneratedValue
private Long id;
private String body;
//private Link link;
}
public class Vote {
@Id @GeneratedValue
private Long id;
private User user;
private Link link;
private int vote;
}
Just a few things to point out about our entities
-
You will need a public no arg constructor.
-
You will need getters and setters for your properties.
-
You will need a toString method for you entity.
-
You will want an equals and hash code method to check for equality.
Common Entity Annotations
When working with Entities these are some of the more common annotations you will be using to provide metadata about your objects.
Project Lombok
Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. Never write another getter or equals method again.
The Lombok website has installation instructions for whatever IDE you are using. It isn’t enough to just install the dependency, you must follow the instructions for your IDE.
If you can’t get the Lombok plugin to work in IntelliJ you will need to make sure annotation processing is turned on for your project.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
Repositories
The central interface in the Spring Data repository abstraction is Repository. It takes the domain class to manage as well as the ID type of the domain class as type arguments. This interface acts primarily as a marker interface to capture the types to work with and to help you to discover interfaces that extend this one.
import com.vega.springit.domain.Link;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LinkRepository extends JpaRepository<Link,Long> {
}
public interface CommentRepository extends JpaRepository<Comment,Long> {
}
Repository Intefaces
| Spring Data also provides persistence technology-specific abstractions, such as JpaRepository or MongoRepository. Those interfaces extend CrudRepository and expose the capabilities of the underlying persistence technology in addition to the rather generic persistence technology-agnostic interfaces such as CrudRepository. |
Custom Queries
Extending one of those repositories will give us a lot of functionality out of the box and is usually enough to get started. When you start building out your application you will undoubtedly need more queries than the base repositories give you. We have a couple ways that we can define our custom queries and we will go over them below.
Named Queries
@Entity
@NamedQuery(name = "User.findByEmailAddress",query = "select u from User u where u.emailAddress = ?1")
public class User {
}
Query Methods
Using named queries to declare queries for entities is a valid approach and works fine for a small number of queries. As the queries themselves are tied to the Java method that executes them, you can actually bind them directly by using the Spring Data JPA @Query annotation rather than annotating them to the domain class. This frees the domain class from persistence specific information and co-locates the query to the repository interface.
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}
The previous example used JPQL. The @Query annotation allows for running native queries by setting the nativeQuery flag to true, as shown in the following:
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
User findByEmailAddress(String emailAddress);
}
The JPA module supports defining a query manually as a String or having it being derived from the method name.
public interface UserRepository extends Repository<User, Long> {
List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}
We will see more of this as we go throughout the course and as we need custom queries.
Entity Relationships (Mappings)
Mappings are a key feature of Hibernate and JPA. They allow us to model the relationships between two tables in your domain objects as properties.
JPA Mapping Annotations
Thoughts on Java
This is a really great article from Thorben Janssen on asoociation mappings in Hibernate. If you’re going to start working with JPA and Hibernate his website Thoughts on Java is a must bookmark.
When we create a bi-directional mapping we have a decision to make. What type should the collection be, a list or a set. This is something to think about and I have included another great article from Thorben on the subject.
Hibernate Tips Book on Amazon
Auditing
Spring Data provides sophisticated support to transparently keep track of who created or changed an entity and when the change happened. To benefit from that functionality, you have to equip your entity classes with auditing metadata that can be defined either using annotations or by implementing an interface.
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastUpdatedDate;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String modifiedBy;
The final step is to enable JPA Auditing and to do so we simply need to add the following annotation to a configuration class.
@SpringBootApplication
@EnableJpaAuditing
public class SpringitApplication {
private static final Logger log = LoggerFactory.getLogger(SpringitApplication.class);
public static void main(String[] args) {
SpringApplication.run(SpringitApplication.class, args);
}
}
This is the easy approach but at some point we will to log the user that created and modified a record. To do so we can simply create a configuration class and make our Enable Jpa Auditing aware of the current logged in user.
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaConfig {
@Bean
public AuditorAware<String> auditorAware() {
return new AuditorAwareImpl();
}
}
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of(((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername());
}
}
If you have a bunch of classes that are going to be auditable you could take this route. Create an abastract class called Auditable and make it a mapped super class. When your class needs to have these auditable features simply extend this class.
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
@CreatedBy
private String createdBy;
@CreatedDate
private LocalDateTime creationDate;
@LastModifiedBy
private String lastModifiedBy;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public LocalDateTime getCreationDate() {
return creationDate;
}
public void setCreationDate(LocalDateTime creationDate) {
this.creationDate = creationDate;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public LocalDateTime getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
@Entity
public class Link extends Auditable { }
Database Layer
In the last section we took a look at how we build out our entities and repositories. We also learned that our entities map directly to a database table. The question that remains is where is this database located? Is it being created for us or is this up to us? We are going to answer these questions and more in this section.
-
Database Configuration
-
Common Database Properties
-
H2 vs MySQL
-
SQL Logging
-
Hibernate logging level DEBUG
-
-
Database Initialization
-
Hibernate DDL
-
schema.sql
-
data.sql
-
-
CommandLineRunner
Common Application Properties
If you can’t always remember what properties can be set in your application.properties there is a really good reference at the link below.
-
DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
-
JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)
-
H2 Web Console
H2
These settings will get us going using H2. Please remember that every time you stop your app you will lose your data, this is just how an in-memory database works.
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
#datasource (default to testdb)
spring.datasource.name=springit
# sa | empty password
#spring.datasource.username=
#spring.datasource.password=
#JPA & Hibernate
#spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
MySQL
In the previous lesson we looked at how to use the H2 in-memory database. That is great for getting started but at some point we will want to move to a production ready database like MySQL. If you are using some other DBMS just do a quick search for what your settings will look like.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
Unlike H2 we need to at least create the database in MySQL. We don’t need to create any of the schema yet, but we need an empty database for us to use.
With a schema in place we can setup our MySQL specific datasource properties.
#DDL mode. This is actually a shortcut for the "hibernate.hbm2ddl.auto"
#property. Defaults to "create-drop" when using an embedded database and no
#schema manager was detected. Otherwise, defaults to "none".
# create | create-drop | none | update | validate
#spring.jpa.hibernate.ddl-auto=
# mysql settings
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mysql://localhost:3306/springit?useSSL=false
spring.datasource.username=springit
spring.datasource.password=password
MySQL Use SSL
Wed Jul 11 07:15:23 EDT 2018 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
With MySQL (and most other DBMS) you will need to give a user permissions to the database you setup. In our case we created a user called springit in MySQL Workbench and gave it privileges to access the database.
Initialize a database
A database can be initialized in different ways depending on what your stack is. Of course, you can also do it manually, provided the database is a separate process.
If you’re using Spring boot 2, database initialization only works for embedded databases (H2, HSQLDB, …). If you want to use it for other databases as well, you need to change the spring.datasource.initialization-mode property:
spring.datasource.initialization-mode=always
Also make sure that you are no longer letting Hibernate create and drop the db for you.
spring.jpa.hibernate.ddl-auto=none
| If you’re going to create a script for database initialization it can’t be empty. If you just create the file and run the application you will see an error that looks like this. |
Invocation of init method failed; nested exception is org.springframework.jdbc.datasource.init.UncategorizedScriptException: Failed to execute database script from resource [URL [file:/Users/vega/dev/spring-boot-2/springit/target/classes/data.sql]]; nested exception is java.lang.IllegalArgumentException: 'script' must not be null or empty
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1710) ~[spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
Command Line Runner
A Command Line Runner
is an interface that contains a single method. It is because of this single method that it is marked as a Functional Interface and we can therefore implement with a Lambda and can avoid creating a separate class to do so.
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* Interface used to indicate that a bean should <em>run</em> when it is contained within
* a {@link SpringApplication}. Multiple {@link CommandLineRunner} beans can be defined
* within the same application context and can be ordered using the {@link Ordered}
* interface or {@link Order @Order} annotation.
* <p>
* If you need access to {@link ApplicationArguments} instead of the raw String array
* consider using {@link ApplicationRunner}.
*
* @author Dave Syer
* @see ApplicationRunner
*/
@FunctionalInterface
public interface CommandLineRunner {
/**
* Callback used to run the bean.
* @param args incoming main method arguments
* @throws Exception on error
*/
void run(String... args) throws Exception;
}
Command Line Runner API:
We could create classes that implement the Command Line Runner if you had the need to do multiple things and or didn’t want to put a ton of extra code in your main application class.
package com.vega.springit;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class DatabaseLoader implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// do some database work
System.out.println("DatabaseLoader...");
}
}
package com.vega.springit;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(2)
public class SomeOtherRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// do some other work
System.out.println("SomeOtherRunner...");
}
}
For this example of just loading a few records though I am going to use my main application class.
@Bean
CommandLineRunner runner() {
return args -> {
};
}
We still need to tell Spring about our Command Line Runner to make this work. In the concrete class example we marked it as a component but that won’t work for us here. The main application is annotated with SpringBootApplication which is made up of @EnableAutoConfiguration which marks this class as a configuration class.
@Configuration indicates that a class declares one or more @Bean methods and may be processed by the Spring container to generate bean definitions and service requests for those beans at runtime
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication { }
@Bean
CommandLineRunner runner(LinkRepository linkRepository, CommentRepository commentRepository) {
return args -> {
Link link = new Link("Getting Started with Spring Boot 2","https://therealdanvega.com/spring-boot-2");
linkRepository.save( link );
Comment comment = new Comment("This Spring Boot 2 Link is awesome",link);
commentRepository.save(comment);
link.addComment(comment);
System.out.println(link.getComments());
};
}
If you want to learn more about the Command Line Runner Interface I wrote up an article on it that you can read below.
Spring MVC: Controller
Controller Introduction
We are now into the C in our MVC application. The C stands for controller and you can think of the controller as the traffic cop in our application. The controller handles incoming http requests and "directs traffic". Methods in your controller are mapped to HTTP by using @RequestingMapping (or one of its composed) annotations.
@Controller Annotation
Spring MVC provides an annotation-based programming model where @Controller and @RestController components use annotations to express request mappings, request input, exception handling, and more. Annotated controllers have flexible method signatures and do not have to extend base classes nor implement specific interfaces. The following example shows a controller defined by annotations:
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
@Controller,@Component, @Service (Stereo-type annotations)
What are all of the annotations I am seeing at the top of classes. I have seen things @Component
, @Controller
, @RestController
, @Service
and I just don’t understand what they are. Luckily I have already written a pretty good article on how these all work. So if you have a chance please read through the following blog post.
@ComponentScan
By default Spring is going to look in your base package for components. If you want to include controllers(components) outside of your base package you need to use @ComponentScan
@SpringBootApplication
@ComponentScan(basePackages = {"com.foo.components"})
public class SpringitApplication {
private static final Logger log = LoggerFactory.getLogger(SpringitApplication.class);
public static void main(String[] args) {
SpringApplication.run(SpringitApplication.class, args);
}
}
@Controller vs @RestController
@RestController is a composed annotation that is itself meta-annotated with @Controller and @ResponseBody to indicate a controller whose every method inherits the type-level @ResponseBody annotation and, therefore, writes directly to the response body versus view resolution and rendering with an HTML template.
If you create a templates folder under resources with an index.html file containing the following code you can switch @RestController
to @Controller
and see the it in action.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Springit</title>
</head>
<body>
<h1>Welcome to SpringIt!</h1>
</body>
</html>
@RequestMapping
You can use the @RequestMapping annotation to map requests to controllers methods. It has various attributes to match by URL, HTTP method, request parameters, headers, and media types. You can use it at the class level to express shared mappings or at the method level to narrow down to a specific endpoint mapping.
@RequestMapping( path = "/", method = RequestMethod.GET, consumes = "application/json", produces = "application.json")
public String home(){
return "Hello, Spring Boot 2!";
}
There are also HTTP method specific shortcut variants of @RequestMapping:
Handler Methods
@RequestMapping handler methods have a flexible signature and can choose from a range of supported controller method arguments and return values. I have included a link to the documentation that covers all of the available handler method arguments.
Link Controller
Now that we know a little bit more about controllers and what methods are available to us we should be able to start building out the basic parts of our Link Controller. Create a new class in the controller package called LinkController.java
. We are going to start with some stub methods and discuss a few things about how to start implementing them.
@RestController
public class LinkController {
// list
public List<Link> list() {
return null;
}
// CRUD
public Link create() {
return null;
}
public Link read() {
return null;
}
public Link update() {
return null;
}
public void delete() {
}
}
Spring MVC: The View Layer
Introduction to views
If we were creating a REST service application we wouldn’t need any view. Instead we are creating a Spring MVC app with a front end using a template engine. As we will see in a bit, we have many choices when it comes to what template engines we can use.
Static Content
By default, Spring Boot serves static content from a directory called /static (or /public or /resources or /META-INF/resources) in the classpath or from the root of the ServletContext.
Welcome Page
Spring Boot supports both static and templated welcome pages. It first looks for an index.html file in the configured static content locations. If one is not found, it then looks for an index template. If either is found, it is automatically used as the welcome page of the application.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Springit</title>
</head>
<body>
<h1>This is my welcome page</h1>
</body>
</html>
Custom Favicon
Spring Boot looks for a favicon.ico in the configured static content locations and the root of the classpath (in that order). If such a file is present, it is automatically used as the favicon of the application.
Templates
Now that we know how to serve static content we need to understand how to serve dynamic content. This is where templates come into play and lucky for us Spring MVC supports a variety of template engines.
View Resolvers
Before we dive into the different template engines we need to understand how templates work behind the scenes. It’s important to undstand that there is magic going on here. The reason I say this is because if you were creating a Spring MVC application from scratch a view resolver is something you would have to manually configured.
A ViewResolver is a core component of Spring MVC, translating view names in @Controller to actual View implementations.
Template Engines
As well as REST web services, you can also use Spring MVC to serve dynamic HTML content. Spring MVC supports a variety of templating technologies, including Thymeleaf, FreeMarker, and JSPs. Also, many other templating engines include their own Spring MVC integrations.
Spring Boot includes auto-configuration support for the following templating engines:
-
FreeMarker
-
Groovy
-
Thymeleaf
-
Mustache
| If possible, JSPs should be avoided. There are several known limitations when using them with embedded servlet containers. |
Getting Started with Thymeleaf
To get started with Thymeleaf you need to select it when you are creating your project
OR add the dependency to your project.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
What is Thymeleaf
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
Thymeleaf’s main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.
Natural Templates
HTML templates written in Thymeleaf still look and work like HTML, letting the actual templates that are run in your application keep working as useful design artifacts.
<table>
<thead>
<tr>
<th th:text="#{msgs.headers.name}">Name</th>
<th th:text="#{msgs.headers.price}">Price</th>
</tr>
</thead>
<tbody>
<tr th:each="prod: ${allProducts}">
<td th:text="${prod.name}">Oranges</td>
<td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
</tr>
</tbody>
</table>
Hello, Thymeleaf!
Now that we know what template engines are in Spring Boot and we have selected Thymeleaf its time we give a quick test run. In our templates folder add a new file called home.html
.
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Springit</title>
</head>
<body>
<h1>Default Welcome Message</h1>
</body>
</html>
Now we are going to add an attribute called title to our model that will be returned to our view for us.
@Controller
public class HomeController {
@GetMapping("/home")
public String home(Model model) {
model.addAttribute("title","Hello, Thymeleaf!");
return "home";
}
}
Remember that Thymeleaf is a natural template engine that allows us to use valid HTML. If we wanted to display our title we can use a data attribute.
<h1 data-th-text="${title}">Default Welcome Message</h1>
This works but it also gets pretty verbose. If we add a namespace to our html file we can use a shorthand.
<html lang="en" xmlns:th="http://www.thymeleaf.org">
Now instead of the data attribute we can just use th:
<h1 th:text="${title}">Default Welcome Message</h1>
Thymeleaf Standard Dialects
The first thing you will notice is that this file is HTML5 that can be correctly displayed by any browser because it does not include any non-HTML tags (browsers ignore all attributes they don’t understand, like th:text).
Template Reloading
If you use Thymeleaf, set spring.thymeleaf.cache to false. See ThymeleafAutoConfiguration for other Thymeleaf customization options.
SpringIt Templates
I mentioned these towards the beginning of the course and it’s finally time to use them. If you missed it earlier I created these so we wouldn’t have to waste time designing some basic templates. The point of this course is to teach you all about Spring Boot, not design. If you haven’t already downloaded them please do so now.
We are going to copy all of the templates over to our static folder.
What we are working on
| Before we get started just a couple of notes before we get started with building out our templates. |
-
We are going to leave the voting mechanism (up vote/down vote) for a later section
-
Because we haven’t talked about Security yet we aren’t going to work on any of the following in this section
-
Login / Logout
-
Register / Account
-
username of who submitted the link
-
Add Comment
-
Thymeleaf Layouts
In our templates, we will often want to include parts from other templates, parts like footers, headers, menus…
In order to do this, Thymeleaf needs us to define these parts, “fragments”, for inclusion, which can be done using the th:fragment attribute.
Say we want to add a head tag to all of our pages pages, we can do so by creating a fragment called head.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Springit - Spring Boot Reddit Clone</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet"<EMAIL_ADDRESS><!-- Custom styles for this template -->
<link rel="stylesheet"<EMAIL_ADDRESS><!-- Google Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Oswald|Roboto" >
<!-- Font Awesome (for icons) -->
<script defer src="https://use.fontawesome.com/releases/v5.0.8/js/all.js"></script>
</head>
<body>
But what if we wanted any page using this layout to pass in its own title. No problem, we can use variables in our fragments as well.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head(title)">
<title th:text="${title}">Springit - Spring Boot Reddit Clone</title>
</head>
<body>
And then in a page using the head fragment we could do this.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layouts/main_layout :: head(title = 'My Unique Title Goes here!')}">
</head>
<body>
There is a bunch we can do with this template approach. I would encourage you to read up on it before we moved forward.
We are going to build a layout page that our templates can use. Create a new folder under templates called layouts and create a main_layout.html.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head(title)">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title th:text="${title}">Springit - Spring Boot Reddit Clone</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet"<EMAIL_ADDRESS><!-- Custom styles for this template -->
<link rel="stylesheet"<EMAIL_ADDRESS><!-- Google Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Oswald|Roboto" >
<!-- Font Awesome (for icons) -->
<script defer src="https://use.fontawesome.com/releases/v5.0.8/js/all.js"></script>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-spring fixed-top" th:fragment="navigation">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fa fa-leaf" aria-hidden="true"></i>
SPRINGIT
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="/">
<i class="fas fa-home" aria-hidden="true"></i> Home
</a>
</li>
<!-- if user is signed in -->
<li class="nav-item">
<a class="nav-link" th:href="@{/link/submit}">
<i class="fa fa-link" aria-hidden="true"></i> Submit Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../auth/profile.html">
<i class="fa fa-user" aria-hidden="true"></i> Account</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<i class="fa fa-sign-out-alt" aria-hidden="true"></i> Sign Out</a>
</li>
<!-- if user is not signed in -->
<li class="nav-item">
<a class="nav-link" href=../auth/login.html>
<i class="fa fa-sign-in-alt" aria-hidden="true"></i> Sign In</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../auth/register.html">
<i class="fa fa-user-plus" aria-hidden="true"></i> Register</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="jumbotron jumbotron-fluid" th:fragment="jumbotron">
<div class="container" align="center">
<h2 class="display-4">Spring Boot Reddit Clone</h2>
<p class="lead">This is a reddit clone built using <a href="#">Spring Boot 2</a>, <a href="#"></a>Spring Framework 5</a> & so many other great projects! If you would like to learn how to build this
application you can check out my course, <a href="#">Getting Started with Spring Boot 2</a>.</p>
<a href="http://www.github.com/cfaddict/springit" class="btn btn-success" target="_blank"><i class="fab fa-github"></i> Github Repo</a>
<a href="https://therealdanvega.teachable.com/p/spring-boot-2" class="btn btn-success" target="_blank"><i class="fas fa-graduation-cap"></i> Spring Boot 2 Course</a>
</div>
</div>
<div class="container" th:fragment="content">
<p>This is your main content</p>
</div>
</body>
</html>
I like to create a new file in this folder as well called new_page.html. This is what a basic starter page will look like.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layouts/main_layout :: head(title = 'Springit - Spring Boot Reddit Clone')}">
</head>
<body>
<nav th:replace="~{layouts/main_layout :: nav}"></nav>
<div th:replace="~{layouts/main_layout :: jumbotron}"></div>
<div th:replace="~{layouts/main_layout :: content}">
<p>This is my content</p>
</div>
</body>
</html>
Building our Link List Page
Now that we have our layout it’s time to build our first page, our list of links. Before we get started it would be a good idea to have a list of links in our database. I went over to one of my favorite sub reddits /r/springboot and just grabbed 11 items for testing.
To do so I am going to move them into our database loader and remove the bean from the main application class.
@Component
public class DatabaseLoader implements CommandLineRunner {
private LinkRepository linkRepository;
private CommentRepository commentRepository;
public DatabaseLoader(LinkRepository linkRepository, CommentRepository commentRepository) {
this.linkRepository = linkRepository;
this.commentRepository = commentRepository;
}
@Override
public void run(String... args) {
Map<String,String> links = new HashMap<>();
links.put("Securing Spring Boot APIs and SPAs with OAuth 2.0","https://auth0.com/blog/securing-spring-boot-apis-and-spas-with-oauth2/?utm_source=reddit&utm_medium=sc&utm_campaign=springboot_spa_securing");
links.put("Easy way to detect Device in Java Web Application using Spring Mobile - Source code to download from GitHub","https://www.opencodez.com/java/device-detection-using-spring-mobile.htm");
links.put("Tutorial series about building microservices with SpringBoot (with Netflix OSS)","https://medium.com/@marcus.eisele/implementing-a-microservice-architecture-with-spring-boot-intro-cdb6ad16806c");
links.put("Detailed steps to send encrypted email using Java / Spring Boot - Source code to download from GitHub","https://www.opencodez.com/java/send-encrypted-email-using-java.htm");
links.put("Build a Secure Progressive Web App With Spring Boot and React","https://dzone.com/articles/build-a-secure-progressive-web-app-with-spring-boo");
links.put("Building Your First Spring Boot Web Application - DZone Java","https://dzone.com/articles/building-your-first-spring-boot-web-application-ex");
links.put("Building Microservices with Spring Boot Fat (Uber) Jar","https://jelastic.com/blog/building-microservices-with-spring-boot-fat-uber-jar/");
links.put("Spring Cloud GCP 1.0 Released","https://cloud.google.com/blog/products/gcp/calling-java-developers-spring-cloud-gcp-1-0-is-now-generally-available");
links.put("Simplest way to Upload and Download Files in Java with Spring Boot - Code to download from Github","https://www.opencodez.com/uncategorized/file-upload-and-download-in-java-spring-boot.htm");
links.put("Add Social Login to Your Spring Boot 2.0 app","https://developer.okta.com/blog/2018/07/24/social-spring-boot");
links.put("File download example using Spring REST Controller","https://www.jeejava.com/file-download-example-using-spring-rest-controller/");
links.forEach((k,v) -> {
linkRepository.save(new Link(k,v));
// we will do something with comments later
});
long linkCount = linkRepository.count();
System.out.println("Number of links in the database: " + linkCount );
}
}
Pretty Time
When we display time I don’t want to just display the creation date. I want to display something like
-
moments ago
-
10 minutes ago
-
1 hour ago
-
7 hours ago
-
1 day ago
-
10 days ago
-
1 year ago
To accomplish this we are going to include a neat little library called Pretty Time.
<dependency>
<groupId>org.ocpsoft.prettytime</groupId>
<artifactId>prettytime</artifactId>
<version>4.0.1.Final</version>
</dependency>
Link Domain Class
These are a few helper methods if you get stuck.
public String getDomainName() throws URISyntaxException {
URI uri = new URI(this.url);
String domain = uri.getHost();
return domain.startsWith("www.") ? domain.substring(4) : domain;
}
public String getPrettyTime() {
PrettyTime pt = BeanUtil.getBean(PrettyTime.class);
return pt.format(convertToDateViaInstant(getCreationDate()));
}
private Date convertToDateViaInstant(LocalDateTime dateToConvert) {
return java.util.Date.from(dateToConvert.atZone(ZoneId.systemDefault()).toInstant());
}
Autowiring Beans into classes not managed by Spring
package com.vega.springit.service;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
@Service
public class BeanUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}
}
This is what our link list page should look like:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layouts/main_layout :: head(title='Springit - Spring Boot Reddit Clone')}">
</head>
<body>
<nav th:replace="~{layouts/main_layout :: nav}"></nav>
<div th:replace="~{layouts/main_layout :: jumbotron}"></div>
<div class="container" th:fragment="content">
<!-- links -->
<div class="row link" th:each="link : ${links}">
<div class="col-1">
<div><a href="#" class="upvote"><i class="fas fa-arrow-up"></i></a></div>
<div><span class="votecount">3</span></div>
<div><a href="#" class="downvote"><i class="fas fa-arrow-down"></i></a></div>
</div>
<div class="col-11">
<p class="title">
<a class="title " th:href="@{'/link/' + ${link.id}}" th:text="${link.title}">
Title Goes Here
</a>
<span class="domain">(<a th:href="${link.url}" th:text="${link.getDomainName()}">therealdanvega.com</a>)</span>
</p>
<p class="tagline ">submitted
<!--<time title="Sat Mar 10 06:26:36 2018 UTC" datetime="2018-03-10T06:26:36+00:00" class="live-timestamp">2 hours ago</time> by-->
<time th:title="${link.getCreationDate()}" th:datetime="${link.getCreationDate()}" class="live-timestamp" th:text="${link.getPrettyTime()}">1 hour(s)</time> by
<a href="/" class="author">therealdanvega</a>
</p>
<p>
<a th:href="@{'/link/' + ${link.id} + '#comments'}" class="comments" th:text="|${link.getComments().size()} comments|"><i class="fas fa-comments"></i> 0 comments</a>
<a href="/" class="share"><i class="fas fa-share-square"></i> share</a>
</p>
</div>
</div>
</div>
</body>
</html>
Building our Link View Page
We already have a working list page and now we need to create a vew page for each link. We will start out by creating a new @GetMapping
in our LinkController
.
@GetMapping("/link/{id}")
public String read(@PathVariable Long id, Model model) {
return "link/view";
}
And we will create a new view page. I did this by renaming link to view and using our layout fragments.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layouts/main_layout :: head(title = 'Springit - Spring Boot Reddit Clone')}">
</head>
<body>
<nav th:replace="~{layouts/main_layout :: nav}"></nav>
<div th:replace="~{layouts/main_layout :: jumbotron}"></div>
<div class="container">
<!-- link -->
<div class="row link">
<div class="col-1">
<div><a href="#" class="upvote"><i class="fas fa-arrow-up"></i></a></div>
<div><span class="votecount">3</span></div>
<div><a href="#" class="downvote"><i class="fas fa-arrow-down"></i></a></div>
</div>
<div class="col-11">
<p class="title">
<a class="title " href="http://therealdanvega.com/blog/2018/03/01/what-is-new-spring-boot-2">
What's new in Spring Boot 2?
</a>
<span class="domain">(<a href="http://therealdanvega.com">therealdanvega.com</a>)</span>
</p>
<p class="tagline ">submitted
<time title="Sat Mar 10 06:26:36 2018 UTC" datetime="2018-03-10T06:26:36+00:00" class="live-timestamp">2 hours ago</time> by
<a href="https://www.reddit.com/user/therealdanvega" class="author">therealdanvega</a>
<span class="userattrs"></span>
</p>
<p>
<a href="#" class="comments"><i class="fas fa-comments"></i> 8 comments</a>
<a href="#" class="share"><i class="fas fa-share-square"></i> share</a>
</p>
</div>
</div>
<!-- link comments -->
<div id="comments" class="comments">
<div class="row header">
<div class="col-1"> </div>
<div class="col-11 title">
all 21 comments
</div>
</div>
<div class="row addcomment">
<div class="col-1"> </div>
<div class="col-11">
<div class="form-group">
<textarea class="form-control" id="comment" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">Add Comment</button>
</div>
</div>
<!-- single comment -->
<div class="row comment">
<div class="col-1"> </div>
<div class="col-11">
<span><a href="#"><i class="far fa-user"></i> therealdanvega</a></span>
<span><i class="far fa-clock"></i> 4 hours ago</span>
<p class="body">It’s one thing I never care about, new releases of maven. Yet I do for most other things.. I really should take a look at any features released in the last while! Pull my dependencies, run my tests and upload to nexus. I don’t care for much else, I wonder what if any I’m missing.</p>
<a href="#" class="permalink"><i class="fas fa-link"></i> permalink</a>
<a href="#" class="like"><i class="far fa-thumbs-up"></i> Like</a>
</div>
</div>
</div>
</div>
</body>
</html>
We need to get an instance of a link based on the id in the url. What happens if that id is something random though like 999? This is why Spring Data wants to return us an Optional by default. In this case we don’t need to do any null checking, we can simply ask the optional if a link is present. If it is we will send it down to our view page, if not we can redirect to our list page.
@GetMapping("/link/{id}")
public String read(@PathVariable Long id,Model model) {
Optional<Link> link = linkRepository.findById(id);
if( link.isPresent() ) {
model.addAttribute("link",link.get());
model.addAttribute("success", model.containsAttribute("success"));
return "link/view";
} else {
return "redirect:/";
}
}
Building our Submit Page
We need to be able to add a new link to our database through the UI. To do so we are going to add a form that allows anyone to enter a title and a URL and submit it. When the submission happens we will perform some validation and if it passes, we will save that link to the database. After the link has been saved we will set a flash attribute and send the user to the view page for that link.
For this to work we are going to need 2 different handler methods in our controller, one to show the page and one to handle the submission. I am also going to add a logger to our page so I can add some logging information in our methods.
private static final Logger logger = LoggerFactory.getLogger(LinkController.class);
@GetMapping("/link/submit")
public String newLinkForm(Model model) {
model.addAttribute("link",new Link());
return "link/submit";
}
@PostMapping("/link/submit")
public String createLink(@Valid Link link, BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes) {
return "link/submit";
}
This is what our submit page is going to look like. The key part to this page is the th:object=""
expression. To learn more about Expressions on selections (asterisk syntax) click here.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layouts/main_layout :: head(title='Springit - Spring Boot Reddit Clone')}">
</head>
<body>
<nav th:replace="~{layouts/main_layout :: nav}"></nav>
<div th:replace="~{layouts/main_layout :: jumbotron}"></div>
<div class="container">
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<h4>Submit Link</h4>
<br/>
<form id="frmAddLink" method="POST" th:action="@{/link/submit}" th:object="${link}">
<div class="form-group">
<label for="title">Title</label>
<input type="text"
class="form-control"
id="title"
placeholder="Enter Title"
required="required"
th:field="*{title}">
<small th:errors="*{title}" style="color:red;"></small>
</div>
<div class="form-group">
<label for="url">URL</label>
<input type="url" class="form-control" id="url" placeholder="URL" required="required"
th:field="*{url}">
<small th:errors="*{url}" style="color:red;"></small>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<div class="col-md-3"></div>
</div>
</div>
</body>
</html>
Validation
We perform validation by adding validation rules to our entity objects. We can use rules like
-
@NotNull
-
@NotEmpty
-
@URL
There are so many validation rules and these are a part of the Hibernate Validation Library.
Link Controller Methods Completed
If we want to we can add a nice success message to our view page
<div class="alert alert-success" role="alert" th:if="${success}">
Your new link was created!
</div>
This is what the final controller methods look like.
@GetMapping("/link/submit")
public String newLinkForm(Model model) {
model.addAttribute("link",new Link());
return "link/submit";
}
@PostMapping("/link/submit")
public String createLink(@Valid Link link, BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes) {
if( bindingResult.hasErrors() ) {
logger.info("Validation errors were found while submitting a new link.");
model.addAttribute("link",link);
return "link/submit";
} else {
// save our link
linkRepository.save(link);
logger.info("New Link was saved successfully.");
redirectAttributes
.addAttribute("id", link.getId())
.addFlashAttribute("success",true);
return "redirect:/link/{id}";
}
}
Spring Security
Spring Security Introduction
| CREATE NEW BRANCH CALLED spring-security |
In this lesson we are going to discuss a very important topic and that is security. It turns out that Security is really hard and it isn’t something that we want to build ourselves. Luckily for us the Spring Security project is the standard for securing Spring based applications and it is really good at it.
There were some important changes in Spring Boot 2 & Spring Security 5 so we are going to dive right into those. In this section we will start by adding the spring boot security starter that will bring in most of everything we will need and from there we will gradually build on that until we have what we need to move forward.
What is Spring Security?
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements
Features
-
Comprehensive and extensible support for both Authentication and Authorization
-
Protection against attacks like session fixation, clickjacking, cross site request forgery, etc
-
Servlet API integration
-
Optional integration with Spring Web MVC
-
Much more…
What’s new in Spring Boot 2.0
Getting Started
To get started with Spring Security you can select it in the Spring Initializer or you can just include the Spring Boot Starter Security.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
| If you cmd+click (ctrl+click on pc) on the artifact you can see what the starter includes. |
If we go ahead and run the application we should see that everything is locked down by default. The reason for this is that this is the default configuration for web security.
/**
* The default configuration for web security. It relies on Spring Security's
* content-negotiation strategy to determine what sort of authentication to use. If the
* user specifies their own {@link WebSecurityConfigurerAdapter}, this will back-off
* completely and the users should specify all the bits that they want to configure as
* part of the custom security configuration.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
public class SpringBootWebSecurityConfiguration {
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
}
}
The default username is user and the password is generated and displayed in the console.
|
Using default security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35
If you want to learn more you can start digging through the Spring Boot Reference docs on security.
Spring Security Configuration
Now that Spring Security is on the class path everything is locked down. We don’t want visitors to have to login just to view data like our home page listing of links and a single link. To do this we need to setup our own security configuration. I am going to create a package called security and create a new class called SecurityConfiguration
that will extend the WebSecurityConfigurerAdapter
.
We are going to mark this class as a @Configuration
class, @EnableWebSecurity
and override the configure(HttpSecurity http)
method.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
Now with that in place we can start to configure our application. If you want to learn more about what we are doing here check out the Spring Security documentation on HttpSecurity.
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
The default configuration above:
-
Ensures that any request to our application requires the user to be authenticated
-
Allows users to authenticate with form based login
-
Allows users to authenticate with HTTP Basic authentication
| Remember that rules are read top down. If you have a rule 1st it will override rules that follow. A quick way to test this is to have a rule that permits any request after the any request must be authenticated rule. |
If we want to setup some roles for our default user we can by using the following property:
spring.security.user.roles=
Users & Roles
In the last lesson we used the default user & auto generated password to test our configuration. While It was good for a quick test we really need to implement something a little bit better. In this lesson we are going to create our User & Role entities and hook them into Spring Security.
Authentication vs Authorization
We have heard these terms a couple of times now so it seems like its a good time to talk about them. Spring Security is both an Authentication
and Authorization
framework so it’s important that we understand what both of those are.
Authentication is the process of determining that somebody is really who they claim to be.
Authorization is the process of verifying that you have access to something. Gaining access to a resource (e.g. a controller method or set of methods) because the permissions configured on it allow you access is authorization.
User & Role Entities
Authentication: User
For Authentication we are going to create a user class. A user class normally has some type of username field and in our case we are going to use the email address as the users username
. The user will also need a password which will be stored in the database as encrypted text. It is never a good idea to store plain text passwords and It’s great to see that Spring Security doesn’t even give us this option.
To hook our user class into Spring Security we are going to implement Spring Security’s UserDetails class. By implementing this this interface we will need to provide a concrete implementation for the following methods.
-
Collection<? extends GrantedAuthority> getAuthorities();
-
String getPassword();
-
String getUsername();
-
boolean isAccountNonExpired();
-
boolean isAccountNonLocked();
-
boolean isCredentialsNonExpired();
-
boolean isEnabled();
public class User implements UserDetails {
@Id @GeneratedValue
private Long id;
@NonNull
@Size(min = 8, max = 20)
@Column(nullable = false, unique = true)
private String email;
@NonNull
@Column(length = 100)
private String password;
@NonNull
@Column(nullable = false)
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "users_roles",
joinColumns = @JoinColumn(name = "user_id",referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id",referencedColumnName = "id")
)
private Set<Role> roles = new HashSet<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
}
public void addRole(Role role) {
roles.add(role);
}
public void addRoles(Set<Role> roles) {
roles.forEach(this::addRole);
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
Authorization: Role
To make sure our user has access to something we will be creating a role class. This is simple class that contains the role name. When this is in place each user will have 1 or more roles that allow them to access certain parts of our application. For instance:
-
A user must contain the
ROLE_USER
role to add a comment -
A user must contain the
ROLE_USER
role to add a new link -
A user must contain the
ROLE_USER
role to vote on a link -
A user must contain the
ROLE_ADMIN
role to view actuator endpoints -
etc…
public class Role {
@Id
@GeneratedValue
private Long id;
@NonNull
private String name;
@ManyToMany( mappedBy = "roles")
private Collection<User> users;
}
User & Role Repositories
We will also create a User & Role Repository.
public interface UserRepository extends JpaRepository<User,Long> {
}
public interface RoleRepository extends JpaRepository<Role,Long> {
}
User Details Service
Core interface which loads user-specific data.It is used throughout the framework as a user DAO and is the strategy used by the DaoAuthenticationProvider.
The first thing we need to do is to override the configure method from our WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private UserDetailsServiceImpl userDetailsService;
public SecurityConfiguration(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
...
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
And then actually create the implementation.
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<User> user = userRepository.findByEmail(email);
if( !user.isPresent() ){
throw new UsernameNotFoundException(email);
}
return user.get();
}
}
public interface UserRepository extends JpaRepository<User,Long> {
Optional<User> findByEmail(String email);
}
DatabaseLoader - Adding Users & Roles
Now that we have everything in place we can add some data for us to use in our development environment. Remember that I told you earlier that we can’t store passwords in plain text, instead we encrypt them so that unwanted eyes can’t see our passwords. Instead of a password of password
in the database you will see something like this.
$2a$10$XPvPcEZrjS35pYtF8M/Q4ewkXkovcB5oiEHi8Uehljkum1tzGr0NG
There are a few password encoders supported by Spring Security but in this example we will be using BCrypt
. In the past we had to pass in a salt when encoding the password but BCrypt will generate a random salt for us. The Spring Security team announced that the PasswordEncoder in org.springframework.security.authentication.encoding as deprecated. The password encoder in org.springframework.security.crypto.password
is what our BCrypt encoder is now using.
/**
* Service interface for encoding passwords.
*
* The preferred implementation is {@code BCryptPasswordEncoder}.
*
* @author Keith Donald
*/
public interface PasswordEncoder {
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
}
In the past you could only use a single password encoding algorithm. Spring Security 5 introduced the concept of password encoding delegation. We can no offer encoding passwords using different algorithms. The way that Spring Security recognizes which algorithm you’re using is by prefixing the password with the encoder. Because we are using BCrypt we will end up storing our passwords in the database like this.
{bcrypt}$2a$10$XPvPcEZrjS35pYtF8M/Q4ewkXkovcB5oiEHi8Uehljkum1tzGr0NG
If the password hash has no prefix, Spring Security will use the default StandardPasswordEncoder
.
private void addUsersAndRoles() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String secret = "{bcrypt}" + encoder.encode("password");
Role userRole = new Role("ROLE_USER");
roleRepository.save(userRole);
Role adminRole = new Role("ROLE_ADMIN");
roleRepository.save(adminRole);
User user = new User("user@gmail.com",secret,true);
user.addRole(userRole);
userRepository.save(user);
User admin = new User("admin@gmail.com",secret,true);
admin.addRole(adminRole);
userRepository.save(admin);
User master = new User("super@gmail.com",secret,true);
master.addRoles(new HashSet<>(Arrays.asList(userRole,adminRole)));
userRepository.save(master);
}
Auditing
Now that we have security in place and we have some users and roles it’s time we revisit something we did a little bit earlier. Remember when we created the Auditable domain class that all of our other domain classes extended? Well we have the created & last updated dates working but now its time we tackle the user fields.
To make this happen we are going to remove the @EnableJpaAuditing
annotation from our main Application class.
@SpringBootApplication
@EnableJpaAuditing
public class SpringitApplication {}
We are then going to create a new config package and a new class called JpaConfig. The auditorAwareRef configures the AuditorAware bean to be used to lookup the current principal.
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaConfig {
@Bean
public AuditorAware<String> auditorAware() {
return new AuditorAwareImpl();
}
}
Next we will create the AuditorAwareImpl
bean. We are implementing the AuditorAware
interface and will need to override a single method called getCurrentAuditor. This will allow us to get the username (email in our case) of the currently logged in user.
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of(((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail());
}
}
If you try and run the application now you are going to run into some issues. Before reading any further do you know what is causing these issues?
The problem here is that we are trying to add some links in our DatabaseLoader
CommandLineRunner. At this point in time there is no logged in user. To make this work for our development environment we need to check to see if there is no authenticated user and if there isn’t hard code an admin user.
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
if(SecurityContextHolder.getContext().getAuthentication() == null ) {
return Optional.of("admin@gmail.com");
} else {
return Optional.of(((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail());
}
}
}
Actuator Security
Now that Spring Boot has backed off of Security we have a little bit of an issue with our actuator endpoints. Right now they are all wide open, so any user can get to them. In some cases this might be ok but in most it’s probably not. Let’s think about some rules that we want to incorporate for our actuator endpoints.
-
The "info" endpoint should be available to anyone
-
The "/actuator" endpoint that lists all available endpoints should be locked down
-
All other endpoints should be locked down.
-
Only users with the ROLE_ADMIN should be able to view the secure data
Spring Boot provides dedicated helpers to make your configuration more readable and explicit. For management endpoints and static resources, Spring Boot provides convenience factories that will supply the right RequestMatcher. For management endpoints, the RequestMatcher will be created based on the management.context-path. Using RequestMatchers gives users the flexibility to secure the application using existing Spring Security expressions such as permitAll, hasRole etc.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers(EndpointRequest.to("info")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")
.antMatchers("/actuator/").hasRole("ACTUATOR")
.and()
.formLogin();
}
The EndpointRequest is especially helpful given that we can change our endpoint mappings from /actuator to something else like /monitoring using the following configuration property. Now if we were to change that our http security stays the same!
management.endpoints.web.base-path=/monitoring
The final thing we need to talk about is the health status endpoint. We have a property that allows us to show-details of the health endpoint. The important thing to remember here is that when_authorized can be any user that is authorized. That is why it’s important to remember to lock that endpoint down to a specific role like we did above.
management.endpoint.health.show-details=when_authorized
H2 Console
If we try and go to our H2 Console (/h2-console) we won’t be able to get there. We need to do a couple of things to make this work. First we need to allow anyone to access /h2-console/**. This is only being used for development so I am fine with this rule for now. We also need to disable CSRF and X-Frame-Options because H2 database console runs inside of a frame.
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers(EndpointRequest.to("info")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR")
.antMatchers("/actuator/").hasRole("ACTUATOR")
.antMatchers("/link/submit").hasRole("USER")
.antMatchers("/link/**").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/h2-console/**").permitAll()
.and()
.formLogin()
.and()
.csrf().disable()
.headers().frameOptions().disable();
}
| MAKE sure we have commits on our branch! |
Spring Security: The View Layer
What we looked at in the last section was how to configure Spring Security. Now that we have a head start on security it’s time we integrate into our view layer. Here is what we are going to cover in this section:
-
Custom Login Form
-
Form Login and Logout
-
Remember Me Feature
-
Thymeleaf Spring Security Dialect
-
Account & Registration Templates
| We are going to keep the same branch, so don’t go merging just yet! |
Form Login & Logout
You might be wondering where the login form came from when you were prompted to log in, since we made no mention of any HTML files or JSPs. Since Spring Security’s default configuration does not explicitly set a URL for the login page, Spring Security generates one automatically, based on the features that are enabled and using standard values for the URL which processes the submitted login, the default target URL the user will be sent to after logging in and so on.
While the automatically generated log in page is convenient to get up and running quickly, most applications will want to provide their own log in page. To do so we can update our configuration as seen below:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") // 1
.permitAll(); // 2
}
-
The updated configuration specifies the location of the log in page.
-
We must grant all users (i.e. unauthenticated users) access to our log in page. The formLogin().permitAll() method allows granting access to all users for all URLs associated with form based log in.
Now we need to setup a request handler for /login. I am going to create an AuthController
that looks like this.
@Controller
public class AuthController {
@GetMapping("/login")
public String login(){
return "auth/login";
}
}
Next we need to update our login template to use our layout. This form contains all of the pieces we will need for our form.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layouts/main_layout :: head(title = 'Springit - Login')}">
</head>
<body>
<nav th:replace="~{layouts/main_layout :: nav}"></nav>
<div th:replace="~{layouts/main_layout :: jumbotron}"></div>
<div class="container">
<!-- login -->
<form class="form-horizontal" role="form" method="POST" th:action="@{/login}">
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<h2>Please Login</h2>
<hr>
<div class="alert alert-danger" th:if="${param.error != null}">
Invalid username and password.
</div>
<div class="alert alert-success" th:if="${param.logout != null}">
You have been logged out.
</div>
</div>
<div class="col-md-3"></div>
</div>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<div class="form-group has-danger">
<label class="sr-only" for="email">E-Mail Address</label>
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
<div class="input-group-addon" style="width: 2.6rem"><i class="fa fa-at"></i></div>
<input type="text" name="email" class="form-control" id="email"<EMAIL_ADDRESS>required autofocus>
</div>
</div>
</div>
<div class="col-md-3"></div>
</div>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<div class="form-group">
<label class="sr-only" for="password">Password</label>
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
<div class="input-group-addon" style="width: 2.6rem"><i class="fa fa-key"></i></div>
<input type="password" name="password" class="form-control" id="password" placeholder="Password" required>
</div>
</div>
</div>
<div class="col-md-3"></div>
</div>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6" style="padding-top: .35rem">
<div class="form-check mb-2 mr-sm-2 mb-sm-0">
<label class="form-check-label">
<input id="remember-me" name="remember-me" class="form-check-input" type="checkbox" >
<span style="padding-bottom: .15rem">Remember me</span>
</label>
</div>
</div>
<div class="col-md-3"></div>
</div>
<div class="row" style="padding-top: 1rem">
<div class="col-md-3"></div>
<div class="col-md-6">
<button type="submit" class="btn btn-success"><i class="fas fa-sign-in-alt"></i> Login</button>
<!-- <a class="btn btn-link" href="forgotPassword.html">Forgot Your Password?</a> -->
</div>
<div class="col-md-3"></div>
</div>
</form>
</div>
</body>
</html>
| We turned off CSRF earlier for the H2 Console to work. We want this on, enable it and read more about it here https://docs.spring.io/spring-security/site/docs/current/reference/html/web-app-security.html#csrf-configure |
Username & Password Form Parameters
There is just one more step to do to make this all work. I bet you’re probably thinking that we need to create a handler method for our login submit but we actually don’t have to. Spring Security handles all of that for us. When we submit a form Spring Security is expecting 2 things from us:
-
The username must be present as the HTTP parameter named username
-
The password must be present as the HTTP parameter named password
Since our field is named "email" we just need to add one more configuration to our form login and that is usernameParameter("email")
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers(EndpointRequest.to("info")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR")
.antMatchers("/actuator/").hasRole("ACTUATOR")
.antMatchers("/link/submit").hasRole("USER")
.antMatchers("/link/**").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/h2-console/**").permitAll()
.and()
.formLogin()
.loginPage("/login").permitAll()
.usernameParameter("email");
}
Form Logout
When using the WebSecurityConfigurerAdapter, logout capabilities are automatically applied. The default is that accessing the URL /logout will log the user out by:
-
Invalidating the HTTP Session
-
Cleaning up any RememberMe authentication that was configured
-
Clearing the SecurityContextHolder
-
Redirect to /login?logout
-
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:
If you wanted to you could further customize the logout experience
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/my/logout")
.logoutSuccessUrl("/my/index")
.logoutSuccessHandler(logoutSuccessHandler)
.invalidateHttpSession(true)
.addLogoutHandler(logoutHandler)
.deleteCookies(cookieNamesToClear)
.and()
...
}
If you get a logout 404 error this is why https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#csrf-logout
I updated our main_layout to use a POST instead of a GET
<li class="nav-item">
<form method="POST" th:action="@{/logout}">
<button class="nav-link"><i class="fa fa-sign-out-alt" aria-hidden="true"></i> Sign Out</button>
</form>
</li>
Fixing our button style
button.nav-link {
background: #2D2A27;
border:none;
cursor: pointer;
}
And the logout started working just fine.
Remember Me
Remember-me or persistent-login authentication refers to web sites being able to remember the identity of a principal between sessions. This is typically accomplished by sending a cookie to the browser, with the cookie being detected during future sessions and causing automated login to take place. Spring Security provides the necessary hooks for these operations to take place, and has two concrete remember-me implementations. One uses hashing to preserve the security of cookie-based tokens and the other uses a database or other persistent storage mechanism to store the generated tokens.
Learn more about both approaches to remember-me:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers(EndpointRequest.to("info")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR")
.antMatchers("/actuator/").hasRole("ACTUATOR")
.antMatchers("/link/submit").hasRole("USER")
.antMatchers("/link/**").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/vote/**").permitAll()
.antMatchers("/h2-console/**").permitAll()
.and()
.formLogin()
.loginPage("/login").permitAll()
.usernameParameter("email")
.and()
.logout()
.and()
.rememberMe()
.and()
.csrf().disable()
.headers().frameOptions().disable();
}
Thymeleaf + Spring Security
In Spring MVC environments, the Spring Security integration module works as a replacement of the Spring security taglib.
We use this dialect in the example in order to print the logged user credentials and to show different content to different roles.
The sec:authorize attribute renders its content when the attribute expression is evaluated to true:
<div sec:authorize="isAuthenticated()">
This content is only shown to authenticated users.
</div>
<div sec:authorize="hasRole('ROLE_ADMIN')">
This content is only shown to administrators.
</div>
<div sec:authorize="hasRole('ROLE_USER')">
This content is only shown to users.
</div>
The sec:authentication attribute is used to print logged user name and roles:
Logged user: <span sec:authentication="name">Bob</span>
Roles: <span sec:authentication="principal.authorities">[ROLE_USER, ROLE_ADMIN]</span>
Getting Started
This is one of the first dependencies that we will come across that isn’t in the Spring Initializer and will need to be added manually. It also worth pointing out that this isn’t officially a part of the Thymeleaf project.
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
| There is something you will need to do for now to get the dialect working. If you’re on Spring Boot 2 and Spring Security 5 you will need to include this in your main application class until Spring Boot’s starter includes it. |
// TODO * Configuring this bean should not be needed once Spring Boot's Thymeleaf starter includes configuration
// TODO for thymeleaf-extras-springsecurity5 (instead of thymeleaf-extras-springsecurity4)
@Bean
public SpringSecurityDialect securityDialect() {
return new SpringSecurityDialect();
}
Anywhere that we are going to use the Spring Security Thymeleaf extras we will need to use the correct namespace.
<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
Now, lets update our main layout so only certain buttons are shown when we are logged in.
<!-- if user is signed in -->
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" th:href="@{/link/submit}">
<i class="fa fa-link" aria-hidden="true"></i> Submit Link</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" th:href="@{/profile}">
<i class="fa fa-user" aria-hidden="true"></i> Account</a>
</li>
<li class="nav-item">
<form method="POST" th:action="@{/logout}" sec:authorize="isAuthenticated()">
<button class="nav-link"><i class="fa fa-sign-out-alt" aria-hidden="true"></i> Sign Out</button>
</form>
</li>
<!-- if user is not signed in -->
<li class="nav-item" sec:authorize="!isAuthenticated()">
<a class="nav-link" th:href="@{/login}">
<i class="fa fa-sign-in-alt" aria-hidden="true"></i> Sign In</a>
</li>
<li class="nav-item" sec:authorize="!isAuthenticated()">
<a class="nav-link" th:href="@{/register}">
<i class="fa fa-user-plus" aria-hidden="true"></i> Register</a>
</li>
Who Submitted the link
One thing that I noticed is that we never updated the display to show who actually submitted the link. I will admit that not having a username or a full name on the User object is going to hurt us here. I wanted to try and keep this simple so we are just going to display the Audit information and use the email. I might come back and add this as an assignment for you to work on later.
First we will need to update our home page to show the user who submitted the link.
<p class="tagline ">submitted
<time th:title="${link.getCreationDate()}" th:datetime="${link.getCreationDate()}" class="live-timestamp" th:text="${link.getPrettyTime()}">1 hour(s)</time> by
<a href="/" class="author" th:text="${link.createdBy}">therealdanvega</a>
</p>
Then we also need to update this in the view link.
<p class="tagline ">submitted
<time th:title="${link.getCreationDate()}" th:datetime="${link.getCreationDate()}" class="live-timestamp" th:text="${link.getPrettyTime()}">1 hour(s)</time> by
<a href="/" class="author" th:text="${link.createdBy}">therealdanvega</a>
<span class="userattrs"></span>
</p>
Again not ideal because its showing the email address but for simplicity sake we will keep it for now.
Account & Registration
When we updated the links in the navbar I added some routes to them. The account link goes to /profile
and the Register link goes to /register
.
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" th:href="@{/profile}">
<i class="fa fa-user" aria-hidden="true"></i> Account</a>
</li>
<li class="nav-item" sec:authorize="!isAuthenticated()">
<a class="nav-link" th:href="@{/register}">
<i class="fa fa-user-plus" aria-hidden="true"></i> Register</a>
</li>
We are going to need a couple of mappings for those and the AuthController
seems like a good place to put them so let’s add these there:
@GetMapping("/profile")
public String profile() {
return "auth/profile";
}
@GetMapping("/register")
public String register() {
return "auth/register";
}
We just need to update those views so they use our layout. We aren’t going to make these actually work just yet, I just want them looking nice.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layouts/main_layout :: head(title = 'Springit - Account Profile Page')}">
</head>
<body>
<nav th:replace="~{layouts/main_layout :: nav}"></nav>
<div th:replace="~{layouts/main_layout :: jumbotron}"></div>
<!-- profile -->
<div id="profile" class="container">
<div class="row">
<ul class="nav">
<li class="nav-item">
<a class="nav-link active" href="#">Overview</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Posts</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Comments</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Preferences</a>
</li>
</ul>
</div>
<div class="row">
<div class="col-8">
<div class="row">
<div class="card col-12" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">What's new in Spring Boot 2</h5>
<h6 class="card-subtitle mb-2 text-muted">submitted 7 days ago by therealdanvega</h6>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="card-link">8 Comments</a>
<a href="#" class="card-link">Share</a>
</div>
</div>
</div>
<div class="row">
<div class="card col-12" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">What's new in Spring Boot 2</h5>
<h6 class="card-subtitle mb-2 text-muted">submitted 7 days ago by therealdanvega</h6>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="card-link">8 Comments</a>
<a href="#" class="card-link">Share</a>
</div>
</div>
</div>
<div class="row">
<div class="card col-12" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">What's new in Spring Boot 2</h5>
<h6 class="card-subtitle mb-2 text-muted">submitted 7 days ago by therealdanvega</h6>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="card-link">8 Comments</a>
<a href="#" class="card-link">Share</a>
</div>
</div>
</div>
<div class="row">
<div class="card col-12" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">What's new in Spring Boot 2</h5>
<h6 class="card-subtitle mb-2 text-muted">submitted 7 days ago by therealdanvega</h6>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="card-link">8 Comments</a>
<a href="#" class="card-link">Share</a>
</div>
</div>
</div>
<div class="row">
<div class="card col-12" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">What's new in Spring Boot 2</h5>
<h6 class="card-subtitle mb-2 text-muted">submitted 7 days ago by therealdanvega</h6>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="card-link">8 Comments</a>
<a href="#" class="card-link">Share</a>
</div>
</div>
</div>
</div>
<!-- Right Side -->
<div class="col-3 offset-1">
<div class="row">
<div class="card col-12">
<img class="card-img-top"<EMAIL_ADDRESS>alt="Card image cap">
<div class="card-body">
<p class="card-text">
<a href="/u/therealdanvega">/u/therealdanvega</a>
</p>
<a href="../link/submit.html"><button type="button" class="btn btn-primary btn-lg btn-block">New Post</button></a>
</div>
</div>
</div>
<div class="row">
<div class="card col-12">
<!-- <img class="card-img-top" src="..." alt="Card image cap"> -->
<div class="card-body">
<h5 class="card-title">Your Information</h5>
<p class="card-text">
Posts: 10<br/>
Comments: 3<br/>
User Since: November 23, 2017<br/>
</p>
</div>
</div>
</div>
<div class="row">
<div class="card col-12">
<h5 class="card-title recentLinks">Recent Links Clicked</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item"><a href="#">What's new in Spring Boot 2</a></li>
<li class="list-group-item"><a href="#">10 Features in Java 10</a></li>
<li class="list-group-item"><a href="#">9 Features in Java 9</a></li>
<li class="list-group-item"><a href="#">8 Features in Java 8</a></li>
<li class="list-group-item"><a href="#">7 Reasons not to use Java 7</a></li>
</ul>
</div>
</div>
</div>
</div> <!-- // row -->
</div><!-- // profile -->
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layouts/main_layout :: head(title = 'Springit - Register for an account')}">
</head>
<body>
<nav th:replace="~{layouts/main_layout :: nav}"></nav>
<div th:replace="~{layouts/main_layout :: jumbotron}"></div>
<!-- User Registration form -->
<form class="form-horizontal" role="form" method="POST" action="">
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<h2>Register New User</h2>
<hr>
</div>
</div>
<div class="row">
<div class="col-md-3 field-label-responsive">
<label for="name">Name</label>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
<div class="input-group-addon" style="width: 2.6rem"><i class="fa fa-user"></i></div>
<input type="text" name="name" class="form-control" id="name"
placeholder="John Doe" required autofocus>
</div>
</div>
</div>
<div class="col-md-3">
<div class="form-control-feedback">
<span class="text-danger align-middle">
<!-- Put name validation error messages here -->
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3 field-label-responsive">
<label for="email">E-Mail Address</label>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
<div class="input-group-addon" style="width: 2.6rem"><i class="fa fa-at"></i></div>
<input type="text" name="email" class="form-control" id="email"
<EMAIL_ADDRESS>required autofocus>
</div>
</div>
</div>
<div class="col-md-3">
<div class="form-control-feedback">
<span class="text-danger align-middle">
<!-- Put e-mail validation error messages here -->
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3 field-label-responsive">
<label for="password">Password</label>
</div>
<div class="col-md-6">
<div class="form-group has-danger">
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
<div class="input-group-addon" style="width: 2.6rem"><i class="fas fa-key"></i></div>
<input type="password" name="password" class="form-control" id="password"
placeholder="Password" required>
</div>
</div>
</div>
<div class="col-md-3">
<div class="form-control-feedback">
<span class="text-danger align-middle">
Example Error Message
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3 field-label-responsive">
<label for="password">Confirm Password</label>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="input-group mb-2 mr-sm-2 mb-sm-0">
<div class="input-group-addon" style="width: 2.6rem">
<i class="fas fa-key"></i>
</div>
<input type="password" name="password-confirmation" class="form-control"
id="password-confirm" placeholder="Password" required>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<button type="submit" class="btn btn-success"><i class="fa fa-user-plus"></i> Register</button>
</div>
</div>
</form>
</body>
</html>
Voting
Voting
Before we can work on our voting mechanism we need to discuss what exactly it is and what we want to accomplish here. When you are looking at a list of links or posts on Reddit you have the opportunity to up vote or down vote.
Here is an explanation right from the https://www.reddit.com/wiki/faq [Reddit FAQ] on scores.
How is a submission’s score determined?
A submission’s score is simply the number of upvotes minus the number of downvotes. If five users like the submission and three users don’t it will have a score of 2. Please note that the vote numbers are not "real" numbers, they have been "fuzzed" to prevent spam bots etc. So taking the above example, if five users upvoted the submission, and three users downvote it, the upvote/downvote numbers may say 23 upvotes and 21 downvotes, or 12 upvotes, and 10 downvotes. The points score is correct, but the vote totals are "fuzzed".
Springit Scores
We are going to simplify this so we don’t get out of hand. We are going to allow any user the ability to up vote and down vote a link as many times as they want. The way we are going to go about this though should allow you to make a few changes and prevent this.
Vote Entity & Repository
The first thing we need to do to make our voting mechnaism work is to setup a Vote Entity & Repository. The Entity is going to be pretty simple and contain an id, direction and link. The direction will tell us if this record is an upvote (1) or downvote (-1).
@Entity
@NoArgsConstructor
@RequiredArgsConstructor
@Getter
@Setter
public class Vote extends Auditable {
@Id
@GeneratedValue
private Long id;
@NonNull
private short direction;
@NonNull
@ManyToOne
private Link link;
}
| if you want to prevent users from voting multiple times you would want to store the UserID on the vote to see if they have voted on this link already or not. |
We also need to make a few changes to the Link
Entity. We are adding the association from Link to Vote but this just gives us a list of votes for each Link. If we wanted to know the total we would have to loop through our votes and sum them up right? We can’t just ask for a votes.size()
because that will just give us the number of votes, not the actual vote sum.
To fix this we are going to store a voteCount and start it at 0. We would not want to calculate this for each and every link every single time.
@OneToMany(mappedBy = "link")
private List<Vote> votes = new ArrayList<>();
private int voteCount = 0;
Finally we will create a Repository for our Vote Domain.
public interface VoteRepository extends JpaRepository<Vote,Long> {
}
Vote Controller
Now that we have a Vote Entity and Repository we can begin to work on our controller. We are going to create a new controller called VoteController
and walk through what this actually needs to do.
@RestController
public class VoteController {
private VoteRepository voteRepository;
private LinkRepository linkRepository;
public VoteController(VoteRepository voteRepository, LinkRepository linkRepository) {
this.voteRepository = voteRepository;
this.linkRepository = linkRepository;
}
@Secured({"ROLE_USER"})
@GetMapping("/vote/link/{linkID}/direction/{direction}/votecount/{voteCount}")
public int vote(@PathVariable Long linkID, @PathVariable short direction, @PathVariable int voteCount) {
Optional<Link> optionalLink = linkRepository.findById(linkID);
if( optionalLink.isPresent() ) {
Link link = optionalLink.get();
Vote vote = new Vote(direction,link);
voteRepository.save(vote);
int updatedVoteCount = voteCount + direction;
link.setVoteCount(updatedVoteCount);
linkRepository.save(link);
return updatedVoteCount;
}
return voteCount;
}
}
Now that our controller is done we need to make our front end work.
Up Vote & Down Vote
To make our up vote and down vote actually work we need to do some work on our home page. What we want to do here is add some functionality that will allow the user to click on an arrow and call our VoteController
.
The first thing we need to do is update our code to add a few attributes. This is just going to help us write our script that calls our API.
<div class="col-1">
<div><a href="#" class="upvote" th:data-id="${link.id}"><i class="fas fa-arrow-up"></i></a></div>
<div><span th:id="|votecount-${link.id}|" class="votecount" th:text="${link.getVoteCount()}">0</span></div>
<div><a href="#" class="downvote" th:data-id="${link.id}" th:disabled="true"><i class="fas fa-arrow-down"></i></a></div>
</div>
Next we will create a script that gets all of the vote links and adds a click event listener to them. Finally we get some data together and then call our API.
<script>
const allvotes = document.querySelectorAll(".upvote, .downvote");
for(let i=0; i<allvotes.length; i++) {
let className = allvotes[i].className;
allvotes[i].addEventListener('click', function (event) {
const direction = className === 'upvote' ? 1 : -1;
const linkID = this.getAttribute("data-id");
const voteSum = document.getElementById('votecount-' + linkID);
const voteSumValue = voteSum.innerHTML;
fetch(`http://localhost:8080/vote/link/${linkID}/direction/${direction}/votecount/${voteSumValue}`)
.then(response => response.json())
.then(data => voteSum.innerHTML = data)
.catch(err => console.error(err));
});
}
</script>
| Take a look at the H2 Console and look at the vote_count column! |
Everything is starting to look pretty good but we still have one issue. Nothing is really secure and we have an issue if we aren’t logged in and try to vote.
Security
The first thing we need to fix is the error that is happening when we try and vote when we aren’t logged in. In our AuditorAwareImpl we try and get the authentication object and if it isn’t null we get the user. The problem here is that in this case the authentication object isn’t null but we actually don’t have a logged in user. This is because by default there is an "anonymousUser" and we need to check for that.
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
if(SecurityContextHolder.getContext().getAuthentication() == null || SecurityContextHolder.getContext().getAuthentication().getPrincipal().equals("anonymousUser")) {
return Optional.of("admin@gmail.com");
} else {
return Optional.of(((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail());
}
}
}
Next we want to make sure these click event listeners aren’t created if you’re not logged in.
<script sec:authorize="isFullyAuthenticated()">
Given that our REST API is wide open and its a GET anyone could pop a link into the address bar and vote. We want to prevent that and make sure only logged in users with the role of ROLE_USER
can hit it. To do so we can use an annotation from Spring Security
.
@Secured({"ROLE_USER"})
@GetMapping("/vote/link/{linkID}/direction/{direction}/votecount/{voteCount}")
public int vote(@PathVariable Long linkID, @PathVariable short direction, @PathVariable int voteCount) {
...
}
To enable this we need to add one more annotation to our Security Configuration class.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
...
}
And with that I think we have a pretty solid voting mechanism. We can of course improve upon it but for now I think it works pretty well.
Comments
The last piece of the UI puzzle is comments. We need to be able to list comments out for each link and we want to allow logged in users to add a new comment.
DatabaseLoader
We are going to add something to our CommandLineRunner
so we can add some test data into our application.
links.forEach((k,v) -> {
Link link = new Link(k,v);
linkRepository.save(link);
// we will do something with comments later
Comment spring = new Comment("Thank you for this link related to Spring Boot. I love it, great post!",link);
Comment security = new Comment("I love that you're talking about Spring Security",link);
Comment pwa = new Comment("What is this Progressive Web App thing all about? PWAs sound really cool.",link);
Comment comments[] = {spring,security,pwa};
for(Comment comment : comments) {
commentRepository.save(comment);
link.addComment(comment);
}
});
List Comments
Now that we have some data in our database we need to list out the comments on our link page. Remember back when we added that functionality to our Link domain to get a pretty time? Well we are going to do the same to comment.
public String getPrettyTime() {
PrettyTime pt = BeanUtil.getBean(PrettyTime.class);
return pt.format(convertToDateViaInstant(getCreationDate()));
}
private Date convertToDateViaInstant(LocalDateTime dateToConvert) {
return java.util.Date.from(dateToConvert.atZone(ZoneId.systemDefault()).toInstant());
}
Update the single comment display to loop over the list of comments for this link.
<!-- single comment -->
<div class="row comment" th:each="comment : ${link.getComments()}">
<div class="col-1"> </div>
<div class="col-11">
<span><a href="#" th:text="${comment.getCreatedBy()}"><i class="far fa-user"></i> therealdanvega</a></span>
<span th:text="${comment.getPrettyTime()}"><i class="far fa-clock"></i> 4 hours ago</span>
<p class="body" th:text="${comment.getBody()}">It’s one thing I never care about, new releases of maven. Yet I do for most other things.. I really should take a look at any features released in the last while! Pull my dependencies, run my tests and upload to nexus. I don’t care for much else, I wonder what if any I’m missing.</p>
<a href="#" class="permalink"><i class="fas fa-link"></i> permalink</a>
<a href="#" class="like"><i class="far fa-thumbs-up"></i> Like</a>
</div>
</div>
Add New Comment
The first thing we need to do is to update our add comment form. First, from the controllers perspective we will need to load an empty comment. To do that we will need to modify our read method in our LinkController
.
@GetMapping("/link/{id}")
public String read(@PathVariable Long id,Model model) {
Optional<Link> link = linkRepository.findById(id);
if( link.isPresent() ) {
Link currentLink = link.get();
Comment comment = new Comment();
comment.setLink(currentLink);
model.addAttribute("comment",comment);
model.addAttribute("link",currentLink);