我使用Spring Boot创建了一个简单的MVC示例系统

第一次使用Spring Boot

我非常感到落后,但我开始学习Spring Boot。
在公司里,我们几乎都是使用自己的框架或者Struts2的系统。但是最近关于安全漏洞和微服务化的讨论越来越多,当参加了去年春天的JJUG CCC 2017 春季大会时,到处都在谈论Spring Boot。考虑到网络上有很多信息并且有很多相关的书籍,所以我决定尝试使用Spring Boot。

在实际使用中,我觉得首先可以立即开始并且很容易地添加功能,感觉非常方便。我一边阅读书籍,一边查看官方文档,再通过互联网上搜索信息,很快就能创建一个简单的系统。虽然也遇到了一些困难点…建议尝试运行一些适当的功能,但既然已经开始了,不如制作一个简单的样例系统来学习。

我已经在GitHub上公开了我编写的源代码,希望能对即将开始的人有所帮助。
https://github.com/ewai/spring-boot-mvc-template

※平时主要从事项目管理等工作,离编码有些遥远,可能存在一些不完善,请多多包涵。如果有更好的方法,请告诉我,我将不胜感激。
※设计不太好,请见谅。

待完成的任务

    • Unit Test(Junit)

 

    Integration Test

我计划随时进行创建和更新。

我做了这样一个样本系统。

系统概述

管理图书信息的系统(类似简单的主数据管理系统)

    • 検索、検索結果一覧

 

    • 登録

 

    • 更新

 

    • 削除

 

    ログイン

需要登录才能操作数据。
只有管理员才能进行注册和更新。← TODO 权限方面存在问题。

画面过渡图

image.png

只有在经过认证的情况下,才能显示除首页之外的页面。

暂且观看一下演示。

ユーザーパスワード権限sbtsbt通常ユーザー権限(参照系のみ)adminadmin管理者権限(データ更新が可能)

目录结构

src
  ├─main
  │  ├─java
  │  │  └─info
  │  │      └─ewai
  │  │          └─sbmt
  │  │              ├─config (Security周りの設定など
  │  │              ├─domain (entity, repositoryなど
  │  │              ├─service  (service
  │  │              └─web (controller, validator
  │  │                  └─form (form
  │  └─resources
  │      ├─static
  │      │  ├─css
  │      │  └─img
  │      └─templates (thymleafのテンプレート
  └─test
      └─java
          └─info
              └─ewai
                  └─sbt (TODO Junit

我正在使用標準的軟件包結構。
如果不符合標準,就必須指定@ComponentScan(“xxx”),
起初我不知道這一點,所以自由地創建並陷入困境。

公式文件
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')

build.gradle 文件的 GitHub 源代码

出于公司使用Jetty的原因,尽管默认情况下使用Tomcat,但我们选择了Jetty。

环境

开发环境

    • Java8

 

    • Eclipse Oxygen

 

    windows10

图书馆

    • Spring Boot 1.5.6

 

    Spring Framework 4.3.0 (上記を指定すると使われるバージョン)

应用服务器

    Jetty

数据库

    • MySQL 5.7

 

    Docker

数据库环境

如果只需要查看源代码而不需要尝试运行的话是不必要的,但在尝试移动时则是必要的。

我根据Docker Hub上官方的MySQL镜像创建了一个Dockerfile,可以自动进行DDL和测试数据的注册。
通过从那个Dockerfile进行docker build来创建镜像,MySQL将会启动并包含DDL和测试数据,因此可以立即将系统投入运行。

只需提供一种选项:假设本地已安装Docker。

以下是一个相对简略的构建步骤。


初回

# こちらをcloneするなりダウンロードするなりする
https://github.com/ewai/docker-spring-boot-template-mysql

以下、コマンドの実行

# イメージ作成
docker build -t sbtdb .

# コンテナ作成
docker run -d --name sbtdb -p 3306:3306 sbtdb

これで起動されているはずです。
docker ps -a

statusがUPになっていればOK。
※DDLもテストデータも投入済みです


2回目以降

状態の確認
docker ps -a

statusがExitedになっていたらコンテナを起動する
docker start sbtdb

当status变为UP时,您可以尝试使用MySQL Workbench等工具进行连接。

接続情報
jdbc:mysql://localhost/sbtdb

ユーザー:sbt
パスワード:sbt

image.png

让它动起来试试看

    • ローカルに上記のDB構築する

 

    • Eclipseにソースをimportする(github)

 

    • info.ewai.sbmt.SpringBootTemplateApplicationを実行する (gradle buildしてjarを実行してもOK)

http://localhost:8080/にアクセスする

image.png

关于源代码的说明

登陆认证

相关来源

+ compile('org.springframework.boot:spring-boot-starter-security')

我将使得能够使用Spring Security。

只需添加这个,基本身份验证就会自动应用。我已经创建了一个部分,用于实现登录验证。

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());
        }
    }
}
    • 各静的ファイルが認証不要にする

 

    • “/”へのアクセスのみ認証不要とする

 

    • “/”以外へアクセスが来た場合は”/login”へ遷移させる

 

    • “/logout”へアクセスが来た場合はJSESSIONIDを削除して”/”へ遷移させる

 

    “/login”へpostが来たらパラメータを元にuserServiceでユーザーを取得しパスワードをチェックする
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login() {
        return "login";
    }

这个控制器只是用来跳转到登录页面的。我记得之前好像找到了能够仅通过配置完成的方法,但是忘记了,所以先将这类方法暂时集中在SimpleController.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;
    }
}
public interface UserRepository extends JpaRepository<User, Long> {
    public User findByUsername(String username);
}

其他
信息.ewai.sbmt.领域.用户
信息.ewai.sbmt.领域.权限
登录.html

我已经实现了UserDetails并且几乎按照标准完成了,但是如果将内部系统的用户信息转换并同步,也许可以使用。

可以在样本系统内使用的用户权限

默认情况下插入的数据

ユーザーパスワード権限権限内容sbtsbtROLE_USER通常ユーザー権限(参照系のみ)adminadminROLE_ADMIN管理者権限(データ更新が可能)adminadminACTUATORSpring Boot Actuatorを使える権限

搜索页面

    @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";
    }
    public List<Book> findByBookNameLikeAndTagLike(String bookName, String tag) {
        if (StringUtils.isEmpty(bookName) && (StringUtils.isEmpty(tag))) {
            return this.findAll();
        }
        return this.bookRepository.findByBookNameLikeAndTagLike("%" + bookName + "%", "%" + tag + "%");
    }
public interface BookRepository extends JpaRepository<Book, Long> {

    public List<Book> findByBookNameLikeAndTagLike(String bookName, String tag);
}

使用Like方法指定可以进行模糊搜索,所以我试着使用了它。我以为参数会自动添加百分号%,但实际没添加,所以我加上了%。虽然标准功能中似乎可以轻松进行分页,但我目前还没有实现。

我认为实际上可能会遇到编写复杂SQL的情况,所以我打算创建自定义repository来编写JPQL或SQL。

@PersistenceContext
EntityManager entityManager;
 ~
Query query = entityManager.createQuery("from Book where id = :id")

编辑界面

输入检查

我正在创建自定义验证器并进行检查。

@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");
        }
    }
}
ほぼBook.javaのEntityと同じ

如果想要进行基本的检查,例如必须检查和大小检查,只需要在表单中添加注释,就可以进行检查。但是,我希望能够重复使用表单,而且不希望将检查分散到表单和验证器中,所以我将检查集中在验证器中。

    • グローバルに出すエラーメッセージ(画面全体へのメッセージ)

 

    各フィールドに出すエラーメッセージ

正在设置错误消息。

错误消息存储在messages_ja.properties文件中。

    @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";
    }

在新图书(bookForm)上,正在进行从表单到实体的替换。
有没有更好的方法呢?

显示全局消息
bindingResult.reject(“错误码”)

将错误消息显示到各个字段中。bindingResult.reject(“field”, “errorCode”)

在Controller中,使用BindingResult设置reject,
而在Validator中,使用Errors设置reject。

@Valid @ModelAttribute 图书表单 bookForm, BindingResult 结果

按照这个顺序编写参数部分的定义似乎是规则。
如果将BindingResult放在前面,将会出现错误。
我有点卡壳了。

如果使用@Valid,则会在调用此处之前通过验证器进行预先检查。
因此,我们只检查result.hasErrors()以确定是否有错误。

更新处理

    @Transactional
    public Book save(Book book) {
        return this.bookRepository.save(book);
    }

实际上,我认为其中可能涉及更复杂的处理,但目前只是简单地保存。如果加上@Transactional,当发生异常时将发生回滚。据说会回滚非检查异常(如RuntimeException)。

我已经参考了这个(指的是链接的内容)。

我本来想在Controller的方法上加上@Transactional,但由于页面控制的关系,我会捕获异常进行处理,但这样一来就无法回滚。所以我觉得应该把业务逻辑基本上都放在Service中,然后在这里加上@Transactional。

如果情况复杂,当然可以通过EntityManager来获取并控制Transaction,但基本上希望能够使用注解就可以了,不知道是否可行。

Thymeleaf模板的通用化

如果有对在每个画面都使用的源代码进行更改的情况,就需要对所有页面进行更改……我们已经将这种源代码进行了共通化。

下面是通用化的源代码。

<!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">ログアウト</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>

我已经创建了三个模板,其中th:fragment标签是模板的内容。

    • headの共通で読み込むJS,CSS

 

    • header

 

    footer

由于这是共用文件,所以我原本考虑放在另一个目录中,但无法加载,所以我把它放在了“templates”文件夹中。

<head>
    <th:block th:include="common::head"></th:block>
    <title>Book Search</title>
</head>
<body>
    <th:block th:replace="common::header"></th:block>
  ~~~コンテンツ~~~
    <th:block th:replace="common::footer"></th:block>
</body>
</html>

我們將每個頁面都用在了這樣的方式上。

作为缺点,就是无法像html一样查看设计。个人而言,我会启动应用程序并在运行过程中进行确认。然而,如果设计师希望使用纯html进行确认,可能最好不使用这种方法。

权限相关

只在特定用户中显示

   + compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4:2.1.3.RELEASE')

起初我添加了以下内容,但是无法运行(sec:authorize=”hasRole(‘ROLE_ADMIN’)”会直接显示在html中),降低版本后就可以了。

编译(’org.thymeleaf.extras:thymeleaf-extras-springsecurity4:3.0.2.RELEASE’)

只显示有权限的用户。

<li class="list-group-item" sec:authorize="hasRole('ROLE_ADMIN')"><a href="/book/create" th:href="@{/book/create}" class="btn btn-link" id="link">書籍登録</a></li>

为了让SEC可用,进行了追加。

      xmlns:sec="http://www.springframework.org/schema/security">

将其转化为可执行的jar文件

springBoot {
    executable = true
}

如果将此添加至编译,并构建可执行的jar文件。这意味着什么呢,那就是…

./spring-boot-mvc-template-0.0.1-SNAPSHOT.jar

可以以这样的方式执行。

当您希望在操作系统启动时自动启动某个程序时,可以使用此功能。

如果是CentOS 7的情况下

[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
systemctl enable sbt.service

现在,在操作系统启动时会自动启动。

Spring Boot执行器

   + compile('org.springframework.boot:spring-boot-starter-actuator')

由于只需添加这个,就可以轻松地确认服务器的状态,所以我进行了添加。
在User(UserDetails)的getAuthorities()中,只有持有”ACUTIATOR”权限的用户可以查看,否则无法访问。
在这次设置中,将此权限授予了admin用户,因此使用admin/admin登录后可以查看。

{"status":"UP","diskSpace":{"status":"UP","total":247762329600,"free":125178765312,"threshold":10485760},"db":{"status":"UP","database":"MySQL","hello":1}}

可以看出服务器还在运行,数据库也在运行,并且还有可用的磁盘空间。

当以没有”ACUTIATOR”权限的用户进行参考时,
根据返回的{“status”:”UP”},似乎能够确认服务器是否正在运行。

在本地主机的8080端口下,若没有”ACUTIATOR”权限则无法访问环境变量功能。
发生了错误。

访问被拒绝。用户必须具备以下角色之一:执行器

http://localhost:8080/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)"
   },

除此以外,还可能存在其他终端点。
http://qiita.com/MariMurotani/items/01dafd2978076b5db2f3

似乎可以进行自定义和更改URL端口,但暂时保持原样。

我参考了以下的信息

Spring框架参考文档4.3.0.RELEASE
http://docs.spring.io/spring/docs/4.3.0.RELEASE/spring-framework-reference/htmlsingle/

Spring Boot参考指南1.5.6.RELEASE
http://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/htmlsingle/

通过使用Spring框架开发Java应用程序,彻底入门了Spring。它详细解释了Spring的各种功能,帮助我理解了Spring的工作原理。还简要介绍了Spring Boot。

SpringBoot编程入门

最终了

虽然用Spring Boot可以非常简单且快速地创建项目,但是如果不了解规则的话可能会陷入一些困境。不过,我觉得官方文档、书籍和网上的信息都很丰富,解决问题相对容易。
这次我创建了一个简单的示例系统,但如果要构建实际可用的系统,我觉得还会有各种尝试和困难要克服,但我想继续使用它。
感谢那些在书籍和网上分享各种信息的人们。

bannerAds