I feel like I'm late, but I started studying Spring Boot. Most of the companies used their own frameworks and Struts2 systems, but the vulnerabilities that have been making noise these days and the trend toward microservices, and when I participated in JJUG CCC 2017 Spring in the spring, everywhere Spring Boot I decided to use Spring Boot because there are many stories about it, and there is a lot of information and books on the net.
As a feeling of using it, I realized that it was convenient because I could start immediately and easily add functions. If you read a book, look at the official documents, and look up information on the net, a simple system will be completed in no time. However, there were some addictive points ... It's okay to move the functions appropriately, but I decided to make a simple sample system and study it.
The created source is published on github, so I hope it will be helpful for those who are starting from now on. https://github.com/ewai/spring-boot-mvc-template
We plan to create and update it from time to time.
System for managing book information (System that performs simple master management)
You need to log in to work with your data Only the administrator can register / update ← TODO authority is not working well
Other than the top screen, it can be displayed only when authenticated.
https://ewai.info/sbt/
user | password | Authority |
---|---|---|
sbt | sbt | Normal user authority (reference system only) |
admin | admin | Administrator authority (data can be updated) |
src
├─main
│ ├─java
│ │ └─info
│ │ └─ewai
│ │ └─sbmt
│ │ ├─config (settings around Security, etc.)
│ │ ├─domain (entity,repository etc.
│ │ ├─service (service
│ │ └─web (controller, validator
│ │ └─form (form
│ └─resources
│ ├─static
│ │ ├─css
│ │ └─img
│ └─ templates (thymeleaf templates
└─test
└─java
└─info
└─ewai
└─sbt (TODO Junit
It has a standard package configuration. It seems that you have to specify @ComponentScan ("xxx") if it does not follow the standard. I made it freely without knowing it at first.
Official documentation http://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/htmlsingle/#using-boot-using-the-default-package
build.gradle
~
springBootVersion = '1.5.6.RELEASE'
~
compile("org.webjars:jquery:3.2.1")
compile("org.webjars:bootstrap:3.3.7")
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-jetty')
compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4:3.0.2.RELEASE')
runtime('mysql:mysql-connector-java:5.1.43')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
github source build.gradle
By default, tomcat is used, but Jetty is used because the company uses Jetty.
Library
Application server
DB
I made a Dockerfile that automatically registers DDL and test data based on the official MySQL of Docker Hub. If you do docker build from there and create an image, it will contain DDL and test data. Since MySQL starts up, you can start the system immediately.
The following is a fairly rough construction procedure.
First time
#Clone or download this
https://github.com/ewai/docker-spring-boot-template-mysql
Below, execute the command
#Image creation
docker build -t sbtdb .
#Container creation
docker run -d --name sbtdb -p 3306:3306 sbtdb
It should now be running.
docker ps -a
OK if the status is UP.
* DDL and test data have already been input.
After the second time
Check the status
docker ps -a
Start the container if the status is Exited
docker start sbtdb
When the status becomes UP, you can connect, so try connecting with MySQL Workbench. sbtdb
Connection information
jdbc:mysql://localhost/sbtdb
User: sbt
Password: sbt
Example) MySQL Workbench
build.gradle
+ compile('org.springframework.boot:spring-boot-starter-security')
Enables Spring Security.
Just add this and Basic authentication will be applied automatically. I made that part because I want to authenticate login.
SecurityConfig.java
package info.ewai.sbmt.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import info.ewai.sbmt.service.UserService;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/img/**", "/css/**", "/js/**", "/webjars/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password").permitAll().and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true).permitAll();
}
@Configuration
protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {
@Autowired
UserService userService;
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
}
}
SimpleController.java
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
return "login";
}
It is a controller that only transitions to the login page. I think I found that this kind of thing can be done only by setting, but I forgot, so I once collected those things in SimpleController.java.
UserService.java
@Component
public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StringUtils.isEmpty(username)) {
throw new UsernameNotFoundException("Username is empty");
}
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found for name: " + username);
}
return user;
}
}
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
public User findByUsername(String username);
}
Other info.ewai.sbmt.domain.User info.ewai.sbmt.domain.Authorities login.html
I implemented UserDetails and made it almost as standard, It may be possible to use it by converting and synchronizing the user information of the in-house system.
Data input by default
user | password | Authority | Authority content |
---|---|---|---|
sbt | sbt | ROLE_USER | Normal user authority (reference system only) |
admin | admin | ROLE_ADMIN | Administrator authority (data can be updated) |
admin | admin | ACTUATOR | Privilege to use Spring Boot Actuator |
BookController.java
@RequestMapping(value = "/book", method = RequestMethod.GET)
public String index(Model model) {
List<Book> list = this.bookservice.findAll();
model.addAttribute("booklist", list);
model.addAttribute("bookForm", new BookForm());
return "book";
}
BookService.java
public List<Book> findByBookNameLikeAndTagLike(String bookName, String tag) {
if (StringUtils.isEmpty(bookName) && (StringUtils.isEmpty(tag))) {
return this.findAll();
}
return this.bookRepository.findByBookNameLikeAndTagLike("%" + bookName + "%", "%" + tag + "%");
}
BookRepository.java
public interface BookRepository extends JpaRepository<Book, Long> {
public List<Book> findByBookNameLikeAndTagLike(String bookName, String tag);
}
If you specify Like in the method, you can do an ambiguous search, so I tried using it. I thought that% would be added automatically to the parameter, but I couldn't, so I added%. It seems that you can easily page with standard functions, but I haven't done so for the time being.
Actually, I think that it is possible to create complicated SQL, so I think that I will create a custom repository and write JPQL and SQL.
@PersistenceContext
EntityManager entityManager;
~
Query query = entityManager.createQuery("from Book where id = :id")
I am making a custom Varidator and checking it.
BookValidator.java
@Component
public class BookValidator implements Validator {
@Autowired
BookService bookService;
@Override
public boolean supports(Class<?> clazz) {
return BookForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// required check
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "bookName", "field.required");
// TODO form check
// BookForm form = BookForm.class.cast(target);
// errors.rejectValue("field", "errorCode");
// global message
if (errors.hasErrors()) {
errors.reject("input.error");
}
}
}
BookForm.java
Almost Book.Same as java Entity
For simple checks such as mandatory check and size check, annotate the Form. It is possible to check, but I wanted to reuse Form and I did not want to distribute the check to Form and Validator. Checks are aggregated in Validator.
Error message is set.
The error message is in messages_ja.properties.
BookController.java
@RequestMapping(value = "/book/save", method = RequestMethod.POST)
public String save(@Valid @ModelAttribute BookForm bookForm, BindingResult result, Model model) {
logger.info("save/" + bookForm.getBookId());
if (result.hasErrors()) {
return "book-edit";
}
try {
this.bookservice.save(new Book(bookForm));
} catch (Exception e) {
result.reject("exception.error");
result.reject("using defaultMessage", e.toString());
return "book-edit";
}
return "book-complete";
}
new Book(bookForm) I am switching from form to entity at. Isn't there any good way?
// Show global message bindingResult.reject("errorCode")
// Display error message for each field bindingResult.reject("field", "errorCode")
In Controller, set reject in BindingResult and In Validator, set reject in Errors.
@Valid @ModelAttribute BookForm bookForm, BindingResult result
It seems to be a rule to write the definition of the argument part in this order. If you bring the BindingResult to the front, an error will occur. I got into it a little.
If @Valid is added, this will be called in a state that has been checked in advance by Validator. So I only check result.hasErrors () for errors.
BookService.java
@Transactional
public Book save(Book book) {
return this.bookRepository.save(book);
}
Actually, I think that more complicated processing will come in, but it is just saving. If @Transactional is added, it will be rolled back when an Exception occurs. It seems that unchecked exceptions (RuntimeException, etc.) are rolled back.
I was allowed to reference) http://qiita.com/NagaokaKenichi/items/a279857cc2d22a35d0dd
I thought about adding @Transactional to the Controller method, Due to screen control, Exception is caught and processed, but then it was not rolled back, so I wonder if all the business logic should be attached here by bringing it to Service I thought.
In complicated cases, of course, it seems that Transaction will be taken out from EntityManager and controlled, I wonder if it's okay to use annotations as a basis, or not.
If there is a change in the source that is commonly used on each screen, all pages must be changed ... I shared the source like.
The following is a common source.
common.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.springframework.org/schema/security">
<head>
<!-- common head -->
<th:block th:fragment="head"><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}" rel="stylesheet" />
<link href="/css/common.css"
th:href="@{/css/common.css}" rel="stylesheet" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"
th:src="@{/webjars/jquery/3.2.1/jquery.min.js}"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script></th:block>
</head>
<body>
<div th:fragment="header" class="container" id="header">
<h1><a href="/">Book</a></h1>
<p style="text-align:right;" th:if="${#httpServletRequest.remoteUser != null}">Hello
<span th:text="${#httpServletRequest.remoteUser}" /> |
<a href="/logout">Log out</a> |
<a href="https://github.com/ewai/spring-boot-mvc-template" target="_blank" alt="spring-boot-mvc-template"><img src="img/mark-github.svg" /></a>
</p>
</div>
<div th:fragment="footer" class="container" id="footer">
<ul>
<li><a href="https://github.com/ewai/spring-boot-mvc-template" target="_blank"><img src="img/mark-github.svg" alt="spring-boot-mvc-template"/></a> <a href="https://github.com/ewai/spring-boot-mvc-template">spring-boot-mvc-template</a></li>
<li><a href="https://github.com/ewai/docker-spring-boot-template-mysql">docker-spring-boot-template-mysql</a></li>
</ul>
</div>
</body>
</html>
The template is inside the ** th: fragment ** tag. I made three.
Since it is a common file, I thought about making it a different directory, but it was not read, so it is placed directly under templates.
book.html
<head>
<th:block th:include="common::head"></th:block>
<title>Book Search</title>
</head>
<body>
<th:block th:replace="common::header"></th:block>
~~~content~~~
<th:block th:replace="common::footer"></th:block>
</body>
</html>
I use each page like this.
The disadvantage is that you can't check the design as html. Personally, I start the application and check it while running it, It may be better not to use it when the designer creates it with pure html and checks it.
When displaying only to a specific user
build.gradle
+ compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4:2.1.3.RELEASE')
At first I added the following, but it didn't work (sec: authorize = "hasRole ('ROLE_ADMIN')" was displayed in html as it is) and it worked when I lowered the version.
compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4:3.0.2.RELEASE')
To display only authorized users
index.html
<li class="list-group-item" sec:authorize="hasRole('ROLE_ADMIN')"><a href="/book/create" th:href="@{/book/create}" class="btn btn-link" id="link">Book registration</a></li>
Added so that sec can be used
index.html
xmlns:sec="http://www.springframework.org/schema/security">
https://github.com/thymeleaf/thymeleaf-extras-springsecurity
build.gradle
springBoot {
executable = true
}
If you add this and build it, it becomes an executable jar. What does that mean?
./spring-boot-mvc-template-0.0.1-SNAPSHOT.jar
It is possible to execute it like this.
It can be used when you want to start it automatically when the OS starts.
For centos7
/etc/systemd/system/sbt.service
[Unit]
Description=sbt
After=syslog.target
[Service]
User=sbtuser
ExecStart=/xxx/xxx/spring-boot-mvc-template-0.0.1-SNAPSHOT.jar
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target
cmd
systemctl enable sbt.service
Now it will start automatically when the OS starts.
Spring Boot Actuator
build.gradle
+ compile('org.springframework.boot:spring-boot-starter-actuator')
I added it because you can easily check the server status just by adding this. A user who has "ACUTIATOR" authority in getAuthorities () of User (UserDetails) It seems that you can not see it unless you log in. This time, the admin user has this privilege, so you can see it by logging in with admin / admin.
http://localhost:8080/health
/health
{"status":"UP","diskSpace":{"status":"UP","total":247762329600,"free":125178765312,"threshold":10485760},"db":{"status":"UP","database":"MySQL","hello":1}}
You can see that the server is alive, the DB is alive, and the disk space is still free.
When I tried to refer to it as a user without "ACUTIATOR" authority, Since {"status": "UP"} is returned, you can see if the server is up.
http://localhost:8080/env As expected, if you do not have "ACUTIATOR" authority, you cannot refer to the environment variable system. I got an error.
Access is denied. User must have one of the these roles: ACTUATOR
http://localhost:8080/mappings It seems that a design document can be made.
/mappings
"{[/book],methods=[GET]}":{
"bean":"requestMappingHandlerMapping",
"method":"public java.lang.String info.ewai.sbmt.web.BookController.index(org.springframework.ui.Model)"
},
"{[/book/edit/{bookId}],methods=[GET]}":{
"bean":"requestMappingHandlerMapping",
"method":"public java.lang.String info.ewai.sbmt.web.BookController.edit(info.ewai.sbmt.web.form.BookForm,org.springframework.validation.BindingResult,java.lang.Long,org.springframework.ui.Model)"
},
"{[/book/save],methods=[POST]}":{
"bean":"requestMappingHandlerMapping",
"method":"public java.lang.String info.ewai.sbmt.web.BookController.save(info.ewai.sbmt.web.form.BookForm,org.springframework.validation.BindingResult,org.springframework.ui.Model)"
},
There are likely to be other endpoints as well. http://qiita.com/MariMurotani/items/01dafd2978076b5db2f3
It seems that you can customize it and change the URL port, but leave it as it is.
Spring Framework Reference Documentation 4.3.0.RELEASE http://docs.spring.io/spring/docs/4.3.0.RELEASE/spring-framework-reference/htmlsingle/
Spring Boot Reference Guide 1.5.6.RELEASE http://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/htmlsingle/
Thorough introduction to Spring Java application development with Spring Framework It was helpful to explain and understand the functions of Spring in general. There is also a little written about Spring boot.
Introduction to Spring Boot Programming
It's a Spring boot that's very easy and quick to make, but there were times when I didn't know the rules. However, I felt that the official documents, books, and online information were abundant and relatively easy to solve. This time I made a simple sample system, so I think that there will be various trials and errors and addictions when making a practical system, but I felt that I would like to use it. Thank you to all the people who have posted various information in books and online.
Recommended Posts