《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文件,配置属性
3.首次使用Gradle构建项目:(Gradle或Maven都一样,我第一次使用Gradle)
构建插件的主要功能是把项目打包成一个可执行的超级JAR (uber-JAR) , 包括把应用程序的所有依赖打入JAR文件内,并为JAR添加一个描述文件,其中的内容能让你用java -jar来运行应用程序。
起步依赖:
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的自动配置:
自动配置会做出以下配置决策,它们和之前的例子息息相关。
- 因为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起步依赖
现在在浏览器里打开该应用程序,马上就会看到HTTP基础身份验证对话框。此处的用户名是user,密码是在应用程序每次运行时随机生成后写入日志的,需要查找日志消息(默认写入标准输出) ,找到此类内容:
这太粗糙了,对用户不友好,所以引出了希望修改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下排除该起步依赖是最简单的办法
然后 在Gradle里可以这样添加Log4j:
要设置日志级别你可以创建以logging.level开头的属性,后面是要日志名称。如果根日志级别要设置为WARN,但Spring Security的日志要用DEBUG级别,可以在application.yml里加入以下内容:
4.配置数据库
eg:
3.2.2 应用程序 Bean 的配置外置
3.2.3 使用 Profile 进行配置
Profile是一种条件化配置,基于运行时激活的Profile,会使用或者忽略不同的Bean或配置类。
这里用的@Profile注解要求运行时激活production Profile,这样才能应用该配置。如果production Profile没有激活,就会忽略该配置,而此时缺少其他用于覆盖的安全配置,于是应用自动配置的安全配置。
也可以向application.yml里添加spring.profiles.active属性:
3.3 定制应用程序错误页面
Spring Boot自动配置的默认错误处理器会查找名为error的视图,如果找不到就用默认的白标
错误视图,如下图所示。
因此,最简单的方法就是创建一个自定义视图,让解析出的视图名为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路径。