Spring Initializer
左侧配置项目详情,包括项目使用的依赖管理工具、语言、SpringBoot版本、项目源信息等。
右侧为项目选择需要的功能,各种功能被SpringBoot封装成一个个starter。
比如你想要创建一个web项目,使用jpa作为持久化技术,h2作为开发阶段嵌入式数据库,使用thymeleaf模板,那你就可以添加这些starter。
starter的主要功能就是自动为你引入依赖,如果是我们手工引入依赖,就拿Spring Data Jpa
来说,在没使用SpringBoot时,我们想要使用spring-data-jpa
,我们得先引入JDBC,如果需要事务,你还要引入事务相关的依赖,而且我们要考虑这些依赖版本能不能互相兼容,有没有潜在的兼容性问题。
starter是经过Spring官方测试的一些maven项目而已,它们会自动帮我们引入一些相关依赖,并且我们无需考虑兼容性问题。
如下是我们使用Spring Initializer后得到的pom,jpa、thymeleaf、web、test等相关的依赖都被定义在这些starter中了,通过maven的依赖传递性引入到我们的项目中。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
随便点开一个starter,这里就用thymeleaf的举例,可以看到这个starter里才是实际引入thymeleaf的位置。
<dependencies>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<version>3.0.4.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
所以starter的目的就是将我们从具体的依赖项和依赖项的版本兼容中解脱出来,如果你要web相关的依赖,那么直接使用一个spring-boot-starter-web
,如果你需要jpa相关的功能,那么直接使用spring-boot-starter-data-jpa
。
如果你很介意starter将你不需要的依赖也传导进你的项目中了,或者你需要使用和starter中定义版本不同的依赖,可以使用exclusions
来排除项目。
项目结构
如上是使用Spring Initializer得到的项目结构。当然,controller、domain和repository都是我们后添加进去的。
项目遵循maven和gradle的项目结构,src/main/java
存放java代码,src/test/java
存放测试代码,src/main/resources
存储资源。
static
文件用于存储web项目中的静态资源,templates用存储视图模板,application.properties
是项目的配置文件,BooklistApplication
是项目的启动类。
@SpringBootApplication
public class BooklistApplication {
public static void main(String[] args) {
SpringApplication.run(BooklistApplication.class, args);
}
}
启动类很简单,使用了一个@SpringBootApplication
注解并在主方法中调用SpringApplication.run
启动程序。
@SpringBootApplication
注解代码如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// ...
}
@EnableAutoConfiguration
启用自动配置功能,@ComponentScan
用来扫描包下的类作为Bean,所以我们直接在包下编写@Controller
、@Service
、@Repository
都可以被SpringBoot扫描。
编写一个项目
图书列表项目
实体类
@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;
}
Repository
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findBooksByReader(String reader);
}
Controller
@RequestMapping("/")
public class BookController {
@Autowired
private BookRepository bookRepository;
@GetMapping("/{reader}")
public String readerBooks(@PathVariable String reader, Model model) {
List<Book> books = bookRepository.findBooksByReader(reader);
model.addAttribute("books", books);
return "readingList";
}
@PostMapping("/{reader}")
public String addToReadingList(
@PathVariable("reader") String reader,
Book book
) {
book.setReader(reader);
bookRepository.save(book);
return "redirect:/{reader}";
}
}
模板
<!DOCTYPE html>
<html lang="en">
<head>
<title>Reading List</title>
</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}"></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)}">
You have no books in your list
</div>
<hr>
<h3>Add A Book</h3>
<form method="post">
Title: <input type="text" name="title"><br>
Author: <input type="text" name="author"><br>
isbn: <input type="text" name="isbn"><br>
Description: <textarea type="text" name="description" cols="80"></textarea><br>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
现在这个项目就能运行了,确实没有使用一行配置代码。
自动配置是如何运行的
前面,确实没有使用一行配置代码,项目就跑起来了。前几天我使用SpringMVC编写程序时,光是配置就弄了一下午。
条件化定义Bean
要想了解自动配置是如何运行的,就得知道Spring的条件化Bean定义。
Spring提供了@Conditional
注解,这个注解需要指定一个实现Condition
接口的类,这个类代表一个条件,条件的真假在matches
方法中返回。比如下面的条件用来查看classpath中是否存在JdbcTemplate
然后我们定义Bean时可以这样
这时只有当JdbcTemplate类存在于classpath下,才创建这个MyServiceBean。
Spring还提供了一些方便的条件注解
AutoConfiguration
所以我们就可以大体猜到了,这些自动配置的Bean全是根据条件创建的。
比如之前如果我们想使用Thymeleaf,我们得先定义一个SpringResourceTemplateResolver
Bean,现在SpringBoot使用条件来自动决策是否定义这个Bean,所以便不用我们配置了。
这是ThymeleafAutoConfiguration,它就是一个普通的Configuration类,唯一不同的是,它检测了类路径下是否有需要的类,如果有,这个配置类才生效。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
// ...
}
再看看其中定义视图解析器的代码,也不过是检测,如果不存在thymeleafViewResolver
这个Bean的话就定义。
@Configuration(proxyBeanMethods = false)
static class ThymeleafViewResolverConfiguration {
@Bean
@ConditionalOnMissingBean(name = "thymeleafViewResolver")
ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties,
SpringTemplateEngine templateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine);
resolver.setCharacterEncoding(properties.getEncoding().name());
resolver.setContentType(
appendCharset(properties.getServlet().getContentType(), resolver.getCharacterEncoding()));
resolver.setProducePartialOutputWhileProcessing(
properties.getServlet().isProducePartialOutputWhileProcessing());
resolver.setExcludedViewNames(properties.getExcludedViewNames());
resolver.setViewNames(properties.getViewNames());
// This resolver acts as a fallback resolver (e.g. like a
// InternalResourceViewResolver) so it needs to have low precedence
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
resolver.setCache(properties.isCache());
return resolver;
}
// ...
}