《SpringBoot 实战》


第二章  开发第一个应用程序


任务: 使用springbooot构建一个简单的阅读列表应用程序

技术栈:

  • Spring MVC来处理Web请求,
  • Thymeleaf来定义Web视图,
  • Spring Data JPA来把阅读列表持久化到数据库里,姑且先用嵌入式的H2数据库

注:

有了起步依赖就不需要指定版本号,起步依赖本身的版本是由正在使用的Spring Boot的版本来决定的,而起步依赖则会决定它们引入的传递依赖的版本。


1.新建SpringBoot项目,勾选Web、Thymeleaf和JPA、H2

打开启动类:

@SpringBootApplication   //开启组件扫描和自动配置
public class ChapterTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(ChapterTestApplication.class, args);//负责启动引导应用程序
    }

}

@SpringBootApplication开启了Spring的组件扫描和Spring Boot的自动配置功能。实际
上,@SpringBootApplication将三个有用的注解组合在了一起。 

  • Spring的@Configuration:标明该类使用Spring基于Java的配置。
  • Spring的@ComponentScan:启用组件扫描,这样你写的Web控制器类和其他组件才能被自动发现并注册为Spring应用程序上下文里的Bean。  
  • Spring Boot 的 @EnableAutoConfiguration :就是这一行配置开启了Spring Boot自动配置的魔力,从此用再写成篇的配置了。 

2.application.properties文件,配置属性

springboot启动扫描配置 springboot扫描service_springboot启动扫描配置

3.首次使用Gradle构建项目:(Gradle或Maven都一样,我第一次使用Gradle)

springboot启动扫描配置 springboot扫描service_springboot启动扫描配置_02

 构建插件的主要功能是把项目打包成一个可执行的超级JAR (uber-JAR) , 包括把应用程序的所有依赖打入JAR文件内,并为JAR添加一个描述文件,其中的内容能让你用java -jar来运行应用程序。

起步依赖: 

springboot启动扫描配置 springboot扫描service_应用程序_03

4. 实体类(POJO)

Book类:@Entity注解表明它是一个JPA实体,id属性加了@Id和@GeneratedValue注解,说明这个字段是实体的唯一标识,并且这个字段的值是自动生成的。

package com.ggqq.chaptertest;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String reader;
    private String isbn;
    private String title;
    private String author;
    private String description;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getReader() {
        return reader;
    }

    public void setReader(String reader) {
        this.reader = reader;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

 5.Service层:

定义用于把Book对象持久化到数据库的仓库:因为用了Spring Data JPA,所以要做的就是简单地定义一个接口,扩展一下Spring Data JPA的JpaRepository接口

package com.ggqq.chaptertest;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ReadingListRepository extends JpaRepository<Book,Long> {
    List<Book> findBookByReader(String reader);
}

6.控制层(Controller层)

ReadingListController使用了@Controller注解,这样组件扫描会自动将其注册为Spring应用程序上下文里的一个Bean。它还用了@RequestMapping注解,将其中所有的处理器方法都映射到了“/”这个URL路径上

package com.ggqq.chaptertest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.List;

@Controller
@RequestMapping("/readingList")
public class ReadingListController {
    public static final String reader = "ggqq";
    private ReadingListRepository readingListRepository;

    @Autowired
    public ReadingListController(ReadingListRepository readingListRepository) {
        this.readingListRepository = readingListRepository;
    }

    @RequestMapping(method = RequestMethod.GET)
    public String readerBooks(Model model){
        List<Book> readingList = readingListRepository.findBookByReader(reader);
        if(readingList != null){
            model.addAttribute("books",readingList);
        }
        return "readingList";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addToReadingList(Book book){
        book.setReader(reader);
        readingListRepository.save(book);
        return "redirect:/readingList";
    }
}

该控制器有两个方法。 
1.readersBooks():处理/{reader}上的HTTP GET请求,根据路径里指定的读者,从(通
过控制器的构造器注入的)仓库获取Book列表。随后将这个列表塞入模型,用的键是
books,最后返回readingList作为呈现模型的视图逻辑名称。 
2. addToReadingList():处理/{reader}上的HTTP POST请求,将请求正文里的数据绑定
到一个Book对象上。该方法把Book对象的reader属性设置为读者的姓名,随后通过仓
库的save()方法保存修改后的Book对象,最后重定向到/{reader}(控制器中的另一个方
法会处理该请求) 。

7.前端:Thymeleaf模板

readersBooks()方法最后返回readingList作为逻辑视图名, 为此必须创建该视图。

在src/main/ resources/templates里创建一个名为readingList.html的文件:

<html>
  <head>
    <title>Reading List</title>
    <link rel="stylesheet" th:href="@{/style.css}"></link>
  </head>

  <body>
    <h2>Your Reading List</h2>
    <div th:unless="${#lists.isEmpty(books)}">
      <dl th:each="book : ${books}">
        <dt class="bookHeadline">
          <span th:text="${book.title}">Title</span> by 
          <span th:text="${book.author}">Author</span>
          (ISBN: <span th:text="${book.isbn}">ISBN</span>)
        </dt>
        <dd class="bookDescription">
          <span th:if="${book.description}" 
                th:text="${book.description}">Description</span>
          <span th:if="${book.description eq null}">
                No description available</span>
        </dd>
      </dl>
    </div>
    <div th:if="${#lists.isEmpty(books)}">
      <p>You have no books in your book list</p>
    </div>

    
    <hr/>
    
    <h3>Add a book</h3>
    <form method="POST">
      <label for="title">Title:</label>
        <input type="text" name="title" size="50"></input><br/>
      <label for="author">Author:</label>
        <input type="text" name="author" size="50"></input><br/>
      <label for="isbn">ISBN:</label>
        <input type="text" name="isbn" size="15"></input><br/>
      <label for="description">Description:</label><br/>
        <textarea name="description" cols="80" rows="5"></textarea><br/>
      <input type="submit"></input>
    </form>
    
  </body>
</html>

8.实际效果:

springboot启动扫描配置 springboot扫描service_spring boot_04

 现在理解一下SpringBoot的自动配置:

自动配置会做出以下配置决策,它们和之前的例子息息相关。 

  •  因为Classpath里有H2,所以会创建一个嵌入式的H2数据库Bean,它的类型是javax.sql.DataSource,JPA实现(Hibernate)需要它来访问数据库。 
  •  因为Classpath里有Hibernate(Spring Data JPA传递引入的)的实体管理器,所以自动配置会配置与 Hibernate相关的 Bean,包括 Spring的LocalContainerEntityManager- FactoryBean和JpaVendorAdapter。 
  • 因为Classpath里有Spring Data JPA,所以它会自动配置为根据仓库的接口创建仓库实现。 
  •  因为Classpath里有Thymeleaf,所以Thymeleaf会配置为Spring MVC的视图,包括一个Thymeleaf的模板解析器、模板引擎及视图解析器。视图解析器会解析相对于Classpath根目录的/templates目录里的模板。 
  • 因 为 Classpath 里 有 Spring MVC ( 归 功 于 Web 起 步 依 赖 ) , 所 以 会 配 置 Spring 的DispatcherServlet并启用Spring MVC。 
  • 因为这是一个Spring MVC Web应用程序, 所以会注册一个资源处理器, 把相对于Classpath根目录的/static目录里的静态内容提供出来。 (这个资源处理器还能处理/public、 /resources和/META-INF/resources的静态内容。 ) 
  • 因为Classpath里有Tomcat (通过Web起步依赖传递引用) , 所以会启动一个嵌入式的Tomcat容器,监听8000端口。

第三章  自定义配置


本章内容:两种影响自动配置的方式——使用显式配置进行覆盖和使用属性进行精细化配置。另外还有如何使用Spring Boot提供的钩子引入自定义的错误页。

3.1 覆盖 Spring Boot 自动配置 

3.1.1 保护应用程序 

安全工作:配置Spring Security起步依赖

springboot启动扫描配置 springboot扫描service_springboot启动扫描配置_05

 现在在浏览器里打开该应用程序,马上就会看到HTTP基础身份验证对话框。此处的用户名是user,密码是在应用程序每次运行时随机生成后写入日志的,需要查找日志消息(默认写入标准输出) ,找到此类内容: 

springboot启动扫描配置 springboot扫描service_java_06

 

springboot启动扫描配置 springboot扫描service_springboot启动扫描配置_07

 

这太粗糙了,对用户不友好,所以引出了希望修改Spring Security的一些配置,至少要有一个好看一些的登录页,还要有一个基于数据库或LDAP(Lightweight Directory Access Protocol)用户存储的身份验证服务。 

3.1.2 创建自定义的安全配置

覆盖自动配置很简单,就当自动配置不存在,直接显式地写一段配置。

在Spring Security的场景下,这意味着写一个扩展了WebSecurityConfigurerAdapter的配置类:

package com.ggqq.chaptertest;

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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private ReaderRepository readerRepository;
  
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .antMatchers("/").access("hasRole('READER')")//要求登录者有READER角色
        .antMatchers("/**").permitAll()
      .and()
      .formLogin()
        .loginPage("/login")                                //设置登录表单的路径
        .failureUrl("/login?error=true");
  }
  
  @Override
  protected void configure(
              AuthenticationManagerBuilder auth) throws Exception {
    auth
      .userDetailsService(new UserDetailsService() { //定义自定义UserDetailsService
        @Override
        public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
          UserDetails userDetails = readerRepository.getOne(username);
          if (userDetails != null) {
            return userDetails;
          }
          throw new UsernameNotFoundException("User '" + username + "' not found.");
        }
      });
  }

}

SecurityConfig是个非常基础的Spring Security配置, 尽管如此, 它还是完成了不少安全定
制工作。通过这个自定义的安全配置类,我们让Spring Boot跳过了安全自动配置,转而使用我们的安全配置。 
        扩展了WebSecurityConfigurerAdapter的配置类可以覆盖两个不同的configure()方法。在SecurityConfig里,第一个configure()方法指明, “/” (ReadingListController的方法映射到了该路径)的请求只有经过身份认证且拥有READER角色的用户才能访问。其他的所有请求路径向所有用户开放了访问权限。 这里还将登录页和登录失败页 (带有一个error属性)指定到了/login。 
        Spring Security为身份认证提供了众多选项,后端可以是JDBC 、LDAP和内存用户存储。在这个应用程序中,我们会通过JPA用数据库来存储用户信息。第二个configure()方法设置了一个自定义的UserDetailsService,这个服务可以是任意实现了UserDetailsService的类,用于查找指定用户名的用户。提供了一个匿名内部类实现, 简单地调用了注入ReaderRepository (这是一个Spring Data JPA仓库接口) 的findOne()方法。

3.2 通过属性文件外置配置

3.2.1 自动配置微调

1.禁用模板缓存 :spring.thymeleaf.cache设置为false(实时变更)

2.设置server.port属性

3.配置日志:

默认情况下,Spring Boot会用Logback(http://logback.qos.ch)来记录日志,并用INFO级别输出到控制台。一般来说,你不需要切换日志实现;Logback能很好地满足你的需要。但是,如果决定使用Log4j或者Log4j2,那么你只需要修改依赖,引入对应该日志实现的起步依赖,同时排除掉
Logback。 

在Gradle里,在configurations下排除该起步依赖是最简单的办法

springboot启动扫描配置 springboot扫描service_应用程序_08

然后 在Gradle里可以这样添加Log4j: 

springboot启动扫描配置 springboot扫描service_spring_09

要设置日志级别你可以创建以logging.level开头的属性,后面是要日志名称。如果根日志级别要设置为WARN,但Spring Security的日志要用DEBUG级别,可以在application.yml里加入以下内容:

springboot启动扫描配置 springboot扫描service_spring_10

 4.配置数据库

eg:

springboot启动扫描配置 springboot扫描service_springboot启动扫描配置_11

3.2.2 应用程序 Bean 的配置外置  

3.2.3 使用 Profile 进行配置

 Profile是一种条件化配置,基于运行时激活的Profile,会使用或者忽略不同的Bean或配置类。 

springboot启动扫描配置 springboot扫描service_spring boot_12

 这里用的@Profile注解要求运行时激活production Profile,这样才能应用该配置。如果production Profile没有激活,就会忽略该配置,而此时缺少其他用于覆盖的安全配置,于是应用自动配置的安全配置。

也可以向application.yml里添加spring.profiles.active属性: 

springboot启动扫描配置 springboot扫描service_spring boot_13

 3.3 定制应用程序错误页面 

Spring Boot自动配置的默认错误处理器会查找名为error的视图,如果找不到就用默认的白标
错误视图,如下图所示。

springboot启动扫描配置 springboot扫描service_springboot启动扫描配置_14

 因此,最简单的方法就是创建一个自定义视图,让解析出的视图名为error。

这一点归根到底取决于错误视图解析时的视图解析器。 

  • 实现了Spring的View接口的Bean,其 ID为error(由Spring的BeanNameViewResolver所解析) 。 
  • 如果配置了Thymeleaf,则有名为error.html的Thymeleaf模板。 
  • 如果配置了FreeMarker,则有名为error.ftl的FreeMarker模板。 
  • 如果配置了V elocity,则有名为error.vm的V elocity模板。 
  • 如果是用JSP视图,则有名为error.jsp的JSP模板。

eg:在本例中:error.html

<html>
  <head>
    <title>Oops!</title>
    <link rel="stylesheet" th:href="@{/style.css}"></link>
  </head>

  <html>
    <div class="errorPage">
      <span class="oops">Oops!</span><br/>
      <img th:src="@{/MissingPage.png}"></img>
      <p>There seems to be a problem with the page you requested 
         (<span text="${path}">/readingList</span>).</p>
      <p th:text="${'Details: ' + message}"></p>
    </div>
  </html>

</html>

其中有两处特别的信息需要呈现:错误的请求路径和异常消息。但这还不是错误页上的全部细节。默认情况下,Spring Boot会为错误视图提供如下错误属性:

  • timestamp:错误发生的时间。 
  • status:HTTP状态码。 
  • error:错误原因。 
  • exception:异常的类名。 
  • message:异常消息(如果这个错误是由异常引起的) 。 
  • errors:BindingResult异常里的各种错误(如果这个错误是由异常引起的) 。
  • trace:异常跟踪信息(如果这个错误是由异常引起的) 。 
  • path:错误发生时请求的URL路径。