Java Spring Boot makes it easy to run Spring Applications. The Spring Application usually has an embedded Web Server in it.
Java Legacy systems were properly obsessed with having clunky heavy weight clustered Application Servers.
You would build an artefact such as an EAR (Enterprise Archive) file which was a zip containing other zipped artefacts. These artefacts had named like Stateful Enterprise Java Beans (Stateful EJB), Stateless EJB, Session EJB, "Servlets, JSP, JSTL" (WAR - Web Archive). So the various WAR and EJB files were zipped up together into a chunky EAR file and that EAR file was deployed to a chunky Application Server - all very great if you get off on chunky, clunky architecture.
The WAR artefact included tech which generated html pages on the backend and passed them to the browser for each request.
The browsers then became more sophisticated and could ask the server for data without the html markup. These requests were called AJAX requests. Typically JSON was returned to the browser and JavaScript was used to interleave the data into a dynamic changing DOM tree. This gave birth to One Page Apps. This meant we did not need Servlets, JSPs and JSTL as much anymore. The One Page app could just ask for simple JSON and build the pages dynamically in the browser.
In the early days it was a nightmare writing One Page apps as the browsers were not adhering properly to standards so there were a lot of if statements saying if the browser is an evil one of a certain evil version then call these methods, else if it was a half decent browser call other methods. This made coding for One Page apps VERY error prone and hard to maintain. It also made devs hate web coding.
This led to the birth of jQuery. jQuery provided its own API which hid all this underlying evil if else complexity. You could now write your code using jquery and it worked without errors - great. However every time a browser introduced new behaviour or another API jQuery had to be released with the new functionality.
jQuery is still pretty active.
However then came along sophisticated fully fledged JavaScript One Page Frameworks like Angular. They introduced the concept of Polyfills. This is basically a dependency you can add to your bundle of JavaScript code sent to the browser which can translate new language features which may or may not be supported by your browser into something your browser understands. This was EXTREMELY cool. It meant designers could invent new language features which zero browsers supported and then use Polyfils to translate them into something the browser understood. As long as you included the correct Polyfils you had eliminated all pain. In time the browser could support the new language feature and function without the Polyfil.
Of course stuff like Polyfils introduced complexity. If your Polyfill had issues you could not fix that yourself without being a total expert. This increased the divide between those that use the tech and those that create the tech.
Now that the whole server side web page generation was out of fashion the Application Server did not need to be that heavy weight any more. Instead smart people realised that it is better to make components more fine grained - that means you can replicate them when you are suffering performance issues. Instead of clustering heavy weight servers you could just cluster small components where ever you saw a bottle neck - this led to the idea of micro web services and the birth of Spring.
In fact this fine grained mentality became so fine grained that instead of deploying your zipped artefact to a stream lined application server you could now bundle the server itself into your artefact's zip code and run it like this:
java -jar myapp.jar
From this Spring was born. Spring typically bundles a Tomcat Web Server of a few MB into your zip file.
Now Spring was still not that easy to use at that point - it had a lot of configuration which could be a nightmare to set up. Java Spring Boot came to the rescue. Through its bundles of pre-configured dependencies and some smart scanning of the class path it could auto configure your application.
Java Spring Boot defined core dependencies in your pom.xml file with parent in the name. Then at startup special components scanned the class path and automatically configured dependencies. i.e. just adding a certain jar to the classpath (via the Maven config) could auto configure that dependency.
Now you could create Java Spring Boot apps without the previous pain.
Spring Boot invented annotations which which could be added on top of method or field declarations which added behaviour to your code. Behind the scenes Class Loaders scanned the class path for these annotations and was able to do some pretty smart stuff. For example certain annotations could call byte code modification libraries like ASM to modify and add extra byte code to existing compiled byte code - it could do this without compiling source code to byte code.
For example in Spring you have annotations like @Transaction which can inject code to manage transactions and rollback with the database.
So just like Polyfils made life easier for the JavaScript crowd, annotations made life easier for the Java crowd. However again fixing buggy annotations which had issues with ASM byte code modification libraries was not for the faint hearted and increased the divide between those that use tech and those that create tech.
Spring then started growing and it step by step started creating more and more wrappers for providers of specialized tech. For example if you want to use Object Relational Mapping and Java Persistence API you could swap out the Hibernate provider for a Top Link providor by changing some config or adding certain libraries to the class path. Spring also provided best practise and provided a uniform layer to access under lying providers of tech. Spring started to become very specialised and Open Source projects like Netflix's Circuit Breaker were ported into Spring Cloud. Spring Cloud was part of the Spring eco system dealing with massive software systems. Circuit Breaker is about preventing error propagation bringing down a whole system.
Typically Spring caught checked Exceptions and threw its own Runtime Exceptions. This simplified the API - you do not need to explicitly catch Runtime Exceptions which lead to cleaner code. This started a fashion for increased usage of Runtime Exceptions and reduced the obsession on checked Exceptions.
As mentioned in Java we saw that a high performant JVM needs Long Lived instances which live for the duration of the JVM. This helps reduce Stop the World pause durations. It also decouples components. Spring allows you to add @Autowire annotations which instruct the Class Loaders to find candidates for injection and inject a singleton instance of that dependent class into a field (at startup). This is pretty good for making neat performant code. Yes startup can take a bit longer, but once started you have reduced Stop the World Garbage Collections.
Now you understand the dynamics that created Java Spring Boot let us look at the main components of standard Spring Boot.
Here are some of the basic components:
Configuration
Controllers
Services
Repositories
Domains
Helpers
All of these components have different annotations. Let us discuss them one by one:
The boot strap class with the main method has this annotation:
@SpringBootApplication
@SpringBootApplication extends these annotations:
@Configuration
@EnableAutoConfiguration
@ComponentScan
@EnableAutoConfiguration automatically configures Spring depending on what classes are in the classpath (as specified by the pom.xml file of maven).
@ComponentScan allows you to specify which package you want to scan classes for
The starting class looks like:
@EntityScan( basePackages = { "com.js.enigma.domain" } )
@SpringBootApplication
public class StartEnigmaBoot {
private static ConfigurableApplicationContext context;
public static void main( String[] args ) {
Locale.setDefault( Locale.ENGLISH );
context = SpringApplication.run( StartEnigmaBoot.class, args );
}
}
Note:
We specified where the domain classes live using the basePackages attribute of the @EntityScan annotation. Domain classes are used by Object Relational Mapping (ORM) to map a Domain Java class to its corresponding database table.
Usually @Autowired is added to a field to inject a singleton instance of the class:
@Autowired
private StudentService studentService;
If StudentService is an interface it will look for a class that Implements StudentService and instantiate it and inject it. If StudentService is a class it will just inject the class.
However sometimes you do not want to inject a class that implements an interface - you want more controll of the instantiation. When that is the case you can explicitly configure the class that can be injected into an @Autowire field.
First of all the class which has this configuration in it should have the annotation @Configuration.
Then the method where you explicitly instantiate the instance should be marked with @Bean:
@Configuration
@Lazy
public class MyConfiguration {
@Bean
public Student createStudent() {
return new StudentImpl();
}
}
Then else where in other classes you can inject the Student as follows:
@Autowired
Student student;
Note
@Lazy is optional - it defers instantiating the bean until it is first required.
These Classes are Singletons. i.e. if more than one class has them autowired, the same instance will be injected into the fields.
@Component can be used to annotate a helper class that can be consumed by a Service. One of the motivations for refactoring code out of a Service and putting it in a Helper is that, that code is required by more than one Service.
@Service, @Repository, @Controller are called stereotype annotations and extend @Component. They are like markers telling Spring how to manage the life cycle of these different classes.
Notice that we have added @Component to the top of DateHelperImpl:
@Component
public class DateHelperImpl implements DateHelper {
// code removed
}
We can comsume this helper class in a Service by using @Autowired:
@Service
public class SomeServiceImpl implements SomeService {
@Autowired
private DateHelper dateHelper;
// code removed
}
Note that Start up the Class Loader comes across DateHelper. It does the following:
(i) It checks if we have any classes with @Configuration that have an @Bean configured for returning a DateHelper
(ii) If it find a DateHelper it returns it. In our example we have no such Configuration bean defined.
(iii) It looks for a class called DateHelper. In our example we have no class called DateHelper. DateHelper is an interface.
(iv) It looks for a class that implements DateHelper. In our example we have a class called DateHelperimpl.
(v) It checks if it has already got an instantiated DateHelperImpl in memory. If it does it returns it. If it does not it instantiates DateHelperImpl and returns it.
Note
that this process of injecting singletons via the process of auto wiring is called Dependency Injection or Inversion of Control (IOC).
that if it found two different classes that implemented DateHelperImpl it would not know which one to inject into the interface type DateHelper. It would fail with an error.
If you know you have more than 2 classes that implement DateHelper you can specify which one you want to use using the @Qualifier annotation
@Service
public class SomeServiceImpl implements SomeService {
@Autowired
@Qualifier( "dateHelperImpl" )
private DateHelper dateHelper;
// code removed
}
DateHelperImpl.java
package com.js.enigma.helper.date;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import org.springframework.stereotype.Component;
/**
* @author John Dickerson
* @date 14th August 2019
*/
@Component
public class DateHelperImpl implements DateHelper {
@Override
public String getLetterDateFormatForLocale( Date date, Locale locale ) {
return FORMAT_EEEEEE_dd_MMMMM_yyyy_HH_mm.format( date );
}
@Override
public Date setToStartOfDay( Date date ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime( date );
calendar.set( Calendar.HOUR_OF_DAY, 0 );
calendar.set( Calendar.MINUTE, 0 );
calendar.set( Calendar.SECOND, 0 );
calendar.set( Calendar.MILLISECOND, 0 );
return calendar.getTime();
}
@Override
public Date setToEndOfDay( Date date ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime( date );
calendar.set( Calendar.HOUR_OF_DAY, 23 );
calendar.set( Calendar.MINUTE, 59 );
calendar.set( Calendar.SECOND, 59 );
calendar.set( Calendar.MILLISECOND, 999 );
return calendar.getTime();
}
}
Has @Service
Services are often used to access other services or call a repository to retrieve or set data in the database via the domain classes.
Note that we are using the @Transactional annotation:
@Service
public class FixMigrateNotficationsServiceImpl implements FixMigrateNotficationsService {
@Autowired
private ClientNotificationRepository clientNotificationRepository;
@Override
@Transactional
public SaveResponse migrateSetInterviewWhereTextAnswerSet() {
// database related code removed
return new SaveResponse( Boolean.TRUE, null );
}
}
The idea is that if the code throws an exception the transaction with the database will be rolled back. For @Transactional to work you must not swallow exceptions. i.e. the exception has to be thrown to the caller of this method in order for the transaction to be rolled back.
ByteCode modification libraries are used to dynamically add a wrapper proxy class to this Service so that it can catch Exceptions and roll back the transaction if an exception occurs.
FixMigrateNotficationsServiceImpl.java
package com.js.enigma.service.admin.fix.notifications;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.js.enigma.domain.applicant.interview.Interview;
import com.js.enigma.domain.client.notification.ClientNotification;
import com.js.enigma.helper.time.TimeHelper;
import com.js.enigma.repository.client.notification.ClientNotificationRepository;
import com.js.enigma.service.shared.SaveResponse;
/**
* @author John Dickerson - 26 Jul 2022
*/
@Service
public class FixMigrateNotficationsServiceImpl implements FixMigrateNotficationsService {
private Logger logger = LoggerFactory.getLogger( FixMigrateNotficationsServiceImpl.class );
@Autowired
private ClientNotificationRepository clientNotificationRepository;
@Autowired
private TimeHelper timeHelper;
@Override
@Transactional
public SaveResponse migrateSetInterviewWhereTextAnswerSet() {
Set<ClientNotification> clientNotifications =
clientNotificationRepository.findByTextAnswerNotNull();
List<ClientNotification> clientNotificationsToSave = new ArrayList<>();
Long nanoStart = System.nanoTime();
for ( ClientNotification clientNotification : clientNotifications ) {
logger.info( "Migratiing clientNotification with id: " + clientNotification.getId() );
Interview interview =
clientNotification.getTextAnswer().getInterviewingSection().getInterview();
clientNotification.setInterview( interview );
clientNotificationsToSave.add( clientNotification );
}
logger.info( "Now saving updated ClientNotifications in one go" );
clientNotificationRepository.saveAll( clientNotificationsToSave );
Long nanoEnd = System.nanoTime();
Long diffSeconds = timeHelper.getDifferenceInSeconds( nanoStart, nanoEnd );
logger.info( "Migration of ClientNotification took: " + diffSeconds + " seconds" );
return new SaveResponse( Boolean.TRUE, null );
}
}
Has @RestController / @Controller
Controllers have paths defined. They also manage role based access.
@RestController
@RequestMapping( "/enigma/client/edit" )
public class ClientEditController {
// code removed
@RequestMapping( value = "/getinitialdata", method = RequestMethod.POST )
@PreAuthorize( "hasAuthority('CLIENT')" )
public ResponseEntity<EditClientAccountInitialData> getInitialData() {
User loggedInUser = loggedInCredentialsHelper.getLoggedInUser();
Client client = clientRepository.findByUserId( loggedInUser.getId() );
EditClientAccountInitialData initialData = editClientService.getEditClientInitialData( client.getId() );
return ResponseEntity.status( HttpStatus.OK ).body( initialData );
}
}
Note:
Note there is a path at the top in the top RequestMapping and that we also have a path in the RequestMapping at the method level. To work out the full path concatenate the two together:
/enigma/client/edit + /getinitialdata = /enigma/client/edit/getinitialdata
Notice that we are checking if the logged in user has the CLIENT authority. If they do not a security exception is thrown. This prevents a logged in user accessing a resource which they do not have rights to access.
In order for this role based authorization to work the Spring runtime searches at startup for a class that implements:
org.springframework.security.core.userdetails.UserDetailsService;
If there is such a class it auto configures the class to decide whether a user can access the resource or not.
In our case we created a class called AppUserDetailsService that implements UserDetailsService:
@Component
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername( String email )
throws UsernameNotFoundException {
String emailLower = email.toLowerCase().trim();
User user = userRepository.findByEmail( emailLower );
if ( user == null ) {
throw new UsernameNotFoundException( String.format(
"The email %s doesn't exist", emailLower ) );
}
Set<Role> roles;
if ( user.getActive() == false ) {
roles = new HashSet<>();
}
else {
roles = user.getRoles();
}
List<GrantedAuthority> authorities = new ArrayList<>();
for ( Role role : roles ) {
authorities.add( new SimpleGrantedAuthority( role.getName() ) );
}
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
emailLower, user.getPasswordHash(), authorities );
return userDetails;
}
}
Note:
that the web browser front end sends the username and password to the backend using the url:
/oauth/token
Spring will automatically pass the username to the above service that implements:
UserDetailsService
In our case the username is an email and we dig out the User domain using the email. The User domain stores a hash of the user's password. We pass the hash of the user's password and the email username to the constructor of the Spring UserDetails class. Internally Spring compares the hash of the password passed in with the hash it calculates itself on the password received from the front end.
This can be a little tricky to follow or setup as one may not be aware of how Spring scans the class path and auto configures components (if it finds a class in the classpath that implements UserDetailsService).
It is also may be tricky to work out what url to send the username and password to from the web side and how to send those details.
The angular web code looks like:
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/internal/operators/map';
import {
TOKEN_AUTH_PASSWORD,
TOKEN_AUTH_USERNAME
} from './auth-constants';
import { ParentService } from 'src/app/parent/parent.service';
@Injectable({
providedIn: 'root'
})
export class AuthenticationService {
static AUTH_TOKEN_URL = '/oauth/token';
constructor(private http: HttpClient,
private parentService: ParentService) {
}
login(username: string, password: string) {
// Set the appropriate header when the user is logged in
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(TOKEN_AUTH_USERNAME + ':' + TOKEN_AUTH_PASSWORD)
})
};
const body =
`username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}` +
`&grant_type=password`;
return this.http.post(AuthenticationService.AUTH_TOKEN_URL, body, httpOptions)
.pipe(map((res: any) => {
if (res.access_token) {
this.parentService.hidePublicHeader = true;
this.parentService.hidePrivateHeader = false;
this.parentService.showNavBar = true;
return res.access_token;
}
return null;
}));
}
}
ClientEditController.java
package com.js.enigma.controller.client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.js.enigma.domain.client.Client;
import com.js.enigma.domain.user.User;
import com.js.enigma.helper.directory.DirectoryHelperException;
import com.js.enigma.helper.loggingin.LoggedInCredentialsHelper;
import com.js.enigma.repository.client.ClientRepository;
import com.js.enigma.service.client.account.edit.EditClientAccountInitialData;
import com.js.enigma.service.client.account.edit.EditClientAccountRequest;
import com.js.enigma.service.client.account.edit.EditClientAccountService;
import com.js.enigma.service.image.SaveImageException;
import com.js.enigma.service.shared.SaveResponse;
/**
* @author John Dickerson
* @date 18 Sep 2019
*/
@RestController
@RequestMapping( "/enigma/client/edit" )
public class ClientEditController {
@Autowired
private EditClientAccountService editClientService;
@Autowired
private LoggedInCredentialsHelper loggedInCredentialsHelper;
@Autowired
private ClientRepository clientRepository;
@RequestMapping( value = "/getinitialdata", method = RequestMethod.POST )
@PreAuthorize( "hasAuthority('CLIENT')" )
public ResponseEntity<EditClientAccountInitialData> getInitialData() {
User loggedInUser = loggedInCredentialsHelper.getLoggedInUser();
Client client = clientRepository.findByUserId( loggedInUser.getId() );
EditClientAccountInitialData initialData = editClientService.getEditClientInitialData( client.getId() );
return ResponseEntity.status( HttpStatus.OK ).body( initialData );
}
@RequestMapping( value = "/save", method = RequestMethod.POST )
@PreAuthorize( "hasAuthority('CLIENT')" )
public ResponseEntity<SaveResponse> editClient(
@RequestPart( "clientDetails" ) EditClientAccountRequest request,
@RequestPart( name = "clientLogo", required = false ) MultipartFile clientLogo )
throws DirectoryHelperException, SaveImageException {
request.setClientLogo( clientLogo );
User loggedInUser = loggedInCredentialsHelper.getLoggedInUser();
SaveResponse saveResponse = editClientService.edit( request, loggedInUser );
return ResponseEntity.status( HttpStatus.OK ).body( saveResponse );
}
}
Uses: @Entity and @Table( name="some_table")
You use these annotations for Object Relational Mapping (ORM) to map domain classes to database tables.
Note that the following example defines Many to Many and Many to One relationships between the User domain and some other domain classes.
It also maps Domain class fields to database columns.
@Entity
@Table( name = "app_user" )
public class User extends AbstractPersistentEntity {
private static final long serialVersionUID = 3926719107314533330L;
@Column( name = "email", nullable = false, unique = true, length = 255 )
private String email;
@Column( name = "first_name", nullable = false, unique = false, length = 100 )
private String firstName;
@Column( name = "last_name", nullable = false, unique = false, length = 100 )
private String lastName;
@ManyToMany( fetch = FetchType.EAGER )
@JoinTable( name = "user_role", joinColumns = @JoinColumn(
name = "user_id",
referencedColumnName = "id",
foreignKey = @ForeignKey( name = "user_role_fk_user" ) ),
inverseJoinColumns = @JoinColumn(
name = "role_id",
foreignKey = @ForeignKey( name = "user_role_fk_role" ),
referencedColumnName = "id" ) )
private Set<Role> roles = new HashSet<Role>();
@ManyToOne( fetch = FetchType.LAZY )
@JoinColumn( name = "fk_domain_organisation",
foreignKey = @ForeignKey( name = "app_user_fk_domain_organization" ),
nullable = false )
private DomainOrganisation domainOrganisation;
@ManyToOne( fetch = FetchType.LAZY )
@JoinColumn( name = "fk_user_type",
foreignKey = @ForeignKey( name = "app_user_fk_user_type" ),
nullable = false )
private UserType userType;
@OneToMany( mappedBy = "user" )
private Set<LoginHistory> loginHistories = new HashSet<>();
// code deleted
}
Note:
The @Entity specifies this is a class used by Object Relational Mapping (ORM)
The @Table annotation specifies which database table this class is mapped to
The @Column maps the domain class field name to a database column. It specifies stuff like whether the field has to be unique or is allowed to be null, and what the length of it is.
We have ManyToOne and ManyToMany relationships defined.
We have OneToMany relationships declared with the mappedBy attribute. Note that when you see the "mappedBy" attribute it means the mapping is declared as a ManyToOne on the other side. i.e. in the above example we know that the mapping is defined in LoginHistory instead of User;
@Entity
@Table( name = "login_history" )
public class LoginHistory extends AbstractPersistentEntity {
private static final long serialVersionUID = -5872023935010277385L;
@Column( name = "ip_address", nullable = false, unique = false,
length = 255 )
private String ipAddress;
@Temporal( TemporalType.TIMESTAMP )
@Column( name = "login_date", updatable = true, nullable = false,
columnDefinition = "TIMESTAMP WITH TIME ZONE" )
private Calendar loginDate;
@ManyToOne( )
@JoinColumn( name = "fk_user",
foreignKey = @ForeignKey( name = "login_history_fk_user" ),
nullable = false )
private User user;
// code deleted
User.java
package com.js.enigma.domain.user;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.js.enigma.domain.AbstractPersistentEntity;
import com.js.enigma.domain.client.jobposting.JobPosting;
import com.js.enigma.domain.login.LoginHistory;
import com.js.enigma.domain.organisation.DomainOrganisation;
import com.js.enigma.domain.requesthistory.RequestHistory;
/**
* @author John Dickerson
* @date 14th August 2019
*/
@Entity
@Table( name = "app_user" )
public class User extends AbstractPersistentEntity {
private static final long serialVersionUID = 3926719107314533330L;
@Column( name = "email", nullable = false, unique = true, length = 255 )
private String email;
@Column( name = "first_name", nullable = false, unique = false, length = 100 )
private String firstName;
@Column( name = "last_name", nullable = false, unique = false, length = 100 )
private String lastName;
@ManyToMany( fetch = FetchType.EAGER )
@JoinTable( name = "user_role", joinColumns = @JoinColumn(
name = "user_id",
referencedColumnName = "id",
foreignKey = @ForeignKey( name = "user_role_fk_user" ) ),
inverseJoinColumns = @JoinColumn(
name = "role_id",
foreignKey = @ForeignKey( name = "user_role_fk_role" ),
referencedColumnName = "id" ) )
private Set<Role> roles = new HashSet<Role>();
@ManyToOne( fetch = FetchType.LAZY )
@JoinColumn( name = "fk_domain_organisation",
foreignKey = @ForeignKey( name = "app_user_fk_domain_organization" ),
nullable = false )
private DomainOrganisation domainOrganisation;
@ManyToOne( fetch = FetchType.LAZY )
@JoinColumn( name = "fk_user_type",
foreignKey = @ForeignKey( name = "app_user_fk_user_type" ),
nullable = false )
private UserType userType;
@OneToMany( mappedBy = "user" )
private Set<LoginHistory> loginHistories = new HashSet<>();
// setters and getters below here
}
Note that you can add some properties to your application properties file which determine whether the entire database schema is recreated at startup:
# spring.jpa.hibernate.ddl-auto=update
# spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.ddl-auto=create-drop
So the create-drop value means the database schema will be generated at startup. Spring is analysing the annotations of the @Entity class and is generating the SQL to create the schema. If you then want production release sql you can use a tool like dBeaver to generate release SQL from the database schema.
Has @Repository
Repositories have methods for accessing, updating domain classes. A domain class is mapped to a database table.
Open InterviewingSectionRepository.java below you will see there are 3 kinds of finders:
(i) This one generates all the SQL just from the name of the finder.
Set<InterviewingSection> findByInterviewIdAndCompleteAndInProgess(
Long interviewId, Boolean complete, Boolean inProgress );
InterviewingSection is a domain class that has a FK to Interview. Interview has the field Id. InterviewingSction has the field complete and inProgress.
This is pretty powerful stuff - you did not even need to write any SQL code at all. Just the name of the finder is enough to generate the code at startup.
If you changed the name of the finder from:
findByInterviewIdAndCompleteAndInProgess
to
findByInterviewIdAndCompleteAndInProgessAndZebra
it would complain at startup that InterviewingSection does not have a filed called Zebra.
(ii) This one is written in HQL. Notice that it is using the domain class names. i.e. InterviewingSection is the Java domain class name. It is not the database table name.
@Query( value = "SELECT COUNT(*) FROM InterviewingSection AS its LEFT JOIN its.interview"
+ " AS i WHERE i.jobPosting= :jobPosting AND "
+ " its.clientQuestionSection = :clientQuestionSection"
+ " AND its.complete = true" )
Long getNumberOfApplicants(
@Param( "jobPosting" ) JobPosting jobPostingId,
@Param( "clientQuestionSection" ) ClientQuestionSection clientQuestionSection );
(iii) Finally we have Native SQL. The Native SQL is placed in a file called orm.xml
@Query( name = "interviewingSection.countCompletedSections", nativeQuery = true )
List<CountCompletedSectionsMapper> getCountCompletedSections( @Param( "jobPostingId" ) Long jobPostingId );
See in orm.xml
<named-native-query
name="interviewingSection.countCompletedSectionsForClient">
<query><![CDATA[
SELECT count(interviewing_section.id) as countz,
interview.id as interviewId
FROM interviewing_section
INNER JOIN interview on interview.id = interviewing_section.fk_interview
WHERE interview.fk_job_posting = :jobPostingId
AND interviewing_section.completed_at is not null
GROUP BY interviewing_section.fk_interview, interview.id;
]]></query>
</named-native-query>
Notice that this is native SQL. It is using the names of database tables and columns instead of the names of domain classes and their fields. The convention is database names and columns use underscores while Domain Classes and fields use camel case.
i.e. the domain class InterviewingSection has the corresponding database table interviewing_section
Similarly the InterviewingSection domain class has the field completedAt while the interviewing_section database table has the field completed_at
InterviewingSectionRepository.java
package com.js.enigma.repository.applicant.interview;
import java.util.List;
import java.util.Set;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import com.js.enigma.domain.applicant.interview.InterviewingSection;
import com.js.enigma.domain.applicant.interview.clientquestionsection.ClientQuestionSection;
import com.js.enigma.domain.client.jobposting.JobPosting;
/**
* @author John Dickerson
* @date 17 Nov 2019
*/
public interface InterviewingSectionRepository extends CrudRepository<InterviewingSection, Long> {
Set<InterviewingSection> findByInterviewIdAndCompleteAndInProgess(
Long interviewId, Boolean complete, Boolean inProgress );
InterviewingSection findByClientQuestionSectionClientQuestionSectionTypeIdAndInterviewId(
Long clienttQuestionSectionTypeId, Long interviewId );
InterviewingSection findByInterviewIdAndClientQuestionSectionId( Long interviewId, Long clientQuestionSectionId );
@Query( value = "SELECT COUNT(*) FROM InterviewingSection AS its LEFT JOIN its.interview"
+ " AS i WHERE i.jobPosting= :jobPosting AND "
+ " its.clientQuestionSection = :clientQuestionSection"
+ " AND its.complete = true" )
Long getNumberOfApplicants(
@Param( "jobPosting" ) JobPosting jobPostingId,
@Param( "clientQuestionSection" ) ClientQuestionSection clientQuestionSection );
@Query( name = "interviewingSection.countCompletedSections", nativeQuery = true )
List<CountCompletedSectionsMapper> getCountCompletedSections( @Param( "jobPostingId" ) Long jobPostingId );
}
orm.xml
<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings
xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence/orm
http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
version="1.0">
<named-native-query
name="interviewingSection.countCompletedSectionsForClient">
<query><![CDATA[
SELECT count(interviewing_section.id) as countz,
interview.id as interviewId
FROM interviewing_section
INNER JOIN interview on interview.id = interviewing_section.fk_interview
WHERE interview.fk_job_posting = :jobPostingId
AND interviewing_section.completed_at is not null
GROUP BY interviewing_section.fk_interview, interview.id;
]]></query>
</named-native-query>
</entity-mappings>
@Configuration - see above
@Bean - see above
@Autowired - see above
@Qualifier - see above
@Lazy - see above
@Value - see below
@PropertySource - see below
@ConfigurationProperty - see below
@Profile - see below
@Scope - see below
When you start Spring Boot you can pass a property defining which profile you are using:
-Dspring.profiles.active=dev
By default Spring will look for an application properties file called:
src/main/resources/application-dev.properties
The properties in application-dev,properties will overide or extend the properties of application.properties
Then in your components, services or controllers you can read properties from your property file.
e.g application-dev.properties could have:
file.provider=S3_FILE_PROVIDER
Then in your class you could read the property as follows:
@Value( "${file.provider}" )
private String fileProvider;
@Value("${spring.profiles.active}")
private String activeProfile;
If you want to read properties from a different file you can also specify the file:
@Configuration
@PropertySource( "classpath:special.properties" )
public class PropertiesConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
Note:
You only need specify the. PropertySourcesPlaceholderConfigurer bean if you are using Spring prior 4.3.0
When you add @Component to your class or use an annotation which extends it like @Service you can specify which profile that class should be injected with.
@Component
@Profile("dev")
public class DevDatasourceConfig implements DatasourceConfig {
@Override
public void setup() {
System.out.println("Setting up datasource for DEV environment. ");
}
}
@Component
@Profile("prod")
public class DevDatasourceConfig implements DatasourceConfig {
@Override
public void setup() {
System.out.println("Setting up datasource for PROD environment. ");
}
}
Then if you have another class with:
@Autowired
private DatasourceConfig datsourceConfig;
it will inject the version of datasourceConfig that matches the profile used to start the application:
-Dspring.profiles.active=prod
We already saw we can specify the profile to use a flag:
-Dspring.profiles.active=prod
However there are alternative ways:
Programatically
SpringApplication.setAdditionalProfiles("dev");
In Maven:
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<profiles>
<profile>dev</profile>
</profiles>
</configuration>
</plugin>
...
</plugins>
This will run Spring Boot from maven:
mvn spring-boot:run
When defining a bean explicitly it is created with Singleton Scope:
@Configuration
public class MyConfiguration {
@Bean
public Student createStudent() {
return new StudentImpl();
}
}
However you can also specify which scope you want it to be created with:
singleton
prototype
request
session
application
websocket
singleton means there will be only one instance of the bean in the enitire JVM, singleton is the default scope if you do not specify it. Note that by having singletons you are creating long lived instances which are great for Performance. There will be no Stop the World garbage pauses if you only have long lived singleton instances.
prototype is for when you want a fresh instance every time:
@Configuration
public class MyConfiguration {
@Bean
@Scope("proptotype")
public Student createStudent() {
return new StudentImpl();
}
}
If you do not want to suffer performance issues with prototypes only keep them for a short time.
The following scopes are for web only:
request - like a singleton but only for request scope
session - like a singleton but only for session scope
application - like a singleton but only for servlet context scope
websocket - like a singleton but only for websocket session
Back: Java
Page Author: JD