Spring 快速参考指南(一)
一、简介
Spring 已经从 2004 年推出(1.0 版)时的一个小型开源项目一飞冲天,成为今天基于 Java 和 JVM 的项目的一个几乎普遍的需求。什么开始作为一个更轻量级的替代 JEE 已经演变成更多,同时仍然保持基本原则。
尽管一些 Spring 子项目,比如 Spring Roo,并没有变得非常受欢迎,但是许多其他项目已经找到了观众并蓬勃发展。“Spring”名下有大量的项目,帮助开发人员完成从云应用到关系数据库查询等各种工作。
Note
我们已经尽了最大努力来确保本书中的信息是准确的,但是由于 Spring 的复杂性以及 Spring 在过去和未来的许多版本,可能会有一些不准确的地方,例如在编写本书时不存在的新功能,这取决于您正在使用的版本。
谁应该读这本书
这本书是为每一个想了解更多关于 Spring 框架、Spring Boot 和相关技术的 Java 开发人员准备的。这本书涵盖了从基础到一些高级主题的所有内容。历史上不会花太多的话;取而代之的是,现在它将关注于开发应用的有用信息。
无论你是初学者还是经验丰富的 Java 专家,这本书都会有用。
关于这本书
这本书是有组织的,所以它可以按顺序阅读,对于那些不熟悉 Spring 的人来说,或者作为未来许多年的参考。每章将涵盖一个 Spring 项目或核心 Spring 框架,并被分成许多有标题的部分。它将涉及配置 Spring Beans、XML、Java 配置类和组件扫描的所有三种方式,但主要关注后两种方式。
这本书将关注核心概念并提供代码示例。例子将是实际的,来自真实世界的经验。
特别重要的信息将概述如下:
Tips
像这样样式的文本提供了额外的信息,您可能会发现非常有用。
Info
这种风格的文本通常会让好奇的读者参考本书之外的其他信息。
Warnings
诸如此类的文字提醒谨慎的读者注意他们可能遇到的常见问题。
Exercises
这是一个练习。我们在实践中学习得最好,所以尝试这些是很重要的。
二、概览
Spring 最初是作为企业应用(如 J2EE 标准)的一种替代方案而出现的。通过允许配置POJO(普通的旧 Java 对象)而不是强制类扩展某个类或实现某个接口,它使得将框架与代码完全分离成为可能。
Spring 随着时间的推移不断成长和发展,是当今构建应用最流行的 Java 框架。
核心 Spring
Core Spring 包括 Spring 的依赖注入(DI)框架和配置。DI 设计模式是一种将依赖关系的细节具体化的方式,允许它们被注入。这一点,再加上接口的使用,允许您解耦代码,并使软件更易于管理和扩展。DI 是反转控制 (IoC)的一个子集,其中应用的流程被反转或颠倒。
Core Spring 提供了 Spring 容器,主要是接口BeanFactory及其子接口ApplicationContext的实现。ApplicationContext有很多种实现方式,使用哪一种取决于应用的类型。大多数时候,你的应用代码不需要知道BeanFactory或ApplicationContext的具体类型;每个应用只应定义一次。
图 2-1
WebApplicationContext 的简化类图
ApplicationContext 提供了用于访问应用组件的 Bean 工厂方法(从ListableBeanFactory interface)继承而来)、以通用方式加载文件资源的能力、向注册的侦听器发布事件的能力(从ApplicationEventPublisher接口继承而来)、解析支持国际化的消息的能力(从MessageSource接口继承而来)以及从父 ApplicationContext 继承而来的可能性。ApplicationContext 有许多不同的子类,其中一个是 WebApplicationContext,顾名思义,它对 web 应用很有用。
POJO 中的 Beans 可以通过三种方式之一进行配置:XML、在用@ Configuration注释的配置 Java 类中用@ Bean注释的方法,或者在使用组件扫描时,可以在 POJO 类本身上添加一个注释,如@ Component或@ Service。最推荐的方法是对基础设施使用一个或多个 Java 配置类,对业务类使用组件扫描。
Spring 模块
Spring 有许多模块,根据应用的需要,可以包含或不包含这些模块。以下是 Spring 保护伞下的一些模块和项目:
-
面向切面编程(AOP)——通过运行时代码交织实现横切关注点。
-
spring Security——认证和授权,支持一系列标准、协议、工具和实践的可配置安全性。
-
spring Data——使用 Java 数据库连接(JDBC)、对象关系映射(orm)工具、反应式关系数据库连接(R2DBC)和 NoSQL 数据库,在 Java 平台上使用关系数据库管理系统的模板和工具。
-
核心——控制容器的反转、应用组件的配置和 Beans 的生命周期管理。
-
消息传递——注册消息侦听器对象,以实现透明的消息消费,并通过多个传输层(包括 Java 消息服务(JMS)、AMQP、Kafka 等)向/从消息队列发送消息。
-
Spring MVC(模型-视图-控制器)——一个基于 HTTP 和 servlet 的框架,为 web 应用和 RESTful(表述性状态转移)web 服务的扩展和定制提供钩子。
-
事务管理——统一多个事务管理 API,协调支持 JTA 和 JXA 的事务。
-
测试——支持编写单元测试和集成测试的类,比如 Spring MVC Test,它支持测试 Spring MVC 应用的控制器。
-
Spring Boot——简化应用开发的配置框架公约。它包括自动配置,并具有“初始”依赖项,包括许多开源依赖项和每个依赖项的兼容版本。
-
spring web flux——一个使用反应流规范的反应式 web 框架,可以在 Netty、Tomcat 或 Jetty 上运行(使用 Servlet 3.0 异步线程)。
图 2-2
Spring 模块
三、依赖注入
依赖注入(DI)是 Spring 的核心。它指的是在运行时在许多不同的对象之间插入引用,或者通过构造函数、设置器,或者甚至使用运行时反射直接到一个字段。这实现了 IOC(控制反转),其中一个类可以使用另一个类的实例,而不知道该对象是如何构造的或其确切的实现类的任何细节。
Spring 的设计允许使用 POJOs(普通旧 Java 对象)。换句话说,你不需要实现一个特定的接口或者扩展一个类来使用 Spring 的 DI。由 Spring 配置的类的实例被称为 Spring Bean ,或者有时简称为 bean 。
退耦
例如,您可以在 Spring Bean 上用@ Autowired注释 setter 或字段,Spring 将在运行时找到与该字段或 setter 最匹配的类。默认情况下,它将搜索与该类型匹配的类。如果它找不到匹配的 bean 或者有不止一个可能的匹配(在考虑任何@ Qualifier注释和名称之后),Spring 将抛出一个异常,无法启动。
您应该使用接口来进一步分离不同的类。这样,不同的组件可以独立测试,而不依赖于其他组件的实现。企业应用中的紧密耦合会导致脆弱的代码,并且很难在不破坏任何东西的情况下进行更改。
您可以使用@ Qualifier指定一个实例的特定名称,以帮助@ Autowired在可能存在同一个类或接口的多个实例时找到正确的实例。我们将在下一节展示一个这样的例子。
配置
可以用三种方式之一配置 Bean:XML,一个用@Configuration注释的配置 Java 类和用@Bean注释的方法,或者在 Bean 类本身上用一个像@Component这样的注释。最推荐的方法是使用一个或多个 Java 配置类。
用@Configuration注释的配置 Java 类可能如下所示:
@Configuration
public class Configuration {
@Bean
public MyService myService() {
return new MyActualService();
}
这个配置创建了配置类本身的一个 bean 实例和实现了MyService接口的类MyActualService的一个名为myService的 bean 实例(来自用@Bean标注的方法)。
任何配置类都必须是非最终的和非局部的(公共的),并且有一个无参数构造函数。默认情况下,Spring 使用 CGLIB 代理该类,以便实施 Spring bean 依赖规则(这就是该类不能是 final 的原因)。例如,这允许方法调用总是返回单例 Bean 实例,而不是每次都创建一个新实例。如果不需要这种行为,可以像下面这样提供proxyBeanMethods=false:
@Configuration(proxyBeanMethods = false)
默认范围是“singleton”,这意味着应用将存在一个类实例或“singleton”。web 应用中还存在其他作用域,如“应用”、“请求”和“会话”。“原型”范围意味着每次请求时都会为 bean 创建一个新的实例。可以使用
@Scope注释来改变 bean 的作用域。例如,@Scope("prototype") @Bean public MyService myService() {...}
用@Bean标注的方法的每个参数都将被 Spring 自动连接(使用适用于@Autowired 的相同规则)。例如,在以下配置中,service2 bean 将连接到 myService bean:
@Configuration
public class Configuration {
@Bean
public MyService myService() {
return new MyActualService();
}
@Bean
public OtherService service2(final MyService myService) {
return new MyActualOtherService(myService);
}
Listing 3-1Configuration.java
默认情况下,Spring 使用方法名作为 Bean 的名称。因此,前面的示例创建了一个名为“myService”的 Bean 和一个名为“service2”的 Bean。您可以通过向@Bean 注释提供一个值来覆盖它(如@Bean("myname"))。
使用@Qualifier,“service 2”方法可以重写如下(结果相同):
@Bean
public OtherService service2(@Qualifier("myService") MyService s) {
return new MyActualOtherService(s);
}
这样,即使存在多个实现 MyService 的 beans,Spring 也会知道选择名为“myService”的那个。
您还可以将一个 bean 配置为具有多个名称。例如,使用
@Bean(name={"myname1", "myname2"})将在两个名称下注册同一个 bean,myname1 和 myname2。
应用上下文
ApplicationContext 是直接公开所有由 Spring 配置的 beans 的接口。
根据应用的类型,它有不同的具体类。例如,web 应用将具有 WebApplicationContext 的实现。
组件扫描
您可以在 Spring 中使用组件扫描来扫描类声明中的某些注释。那些注解是@Component、@Controller、@Service和@Repository(和@Configuration)。如果找到它们,Spring 会将 POJO 初始化为 Spring Bean。
可以通过 XML 配置组件扫描,如下所示:
<context:component-scan base-package="com.example"/>
或者在这样的配置类中:
@Configuration
@ComponentScan("com.example")
public class Configuration {
Listing 3-2Configuration.java
在这些示例中,将扫描“com.example”包及其所有子包中的 Spring 注释来创建 beans。注意不要扫描太多的类,因为这会降低初始化时间。
导入
您可以使用@Import导入其他配置文件。使用@ComponentScan也可以用来扫描配置类(标有@Configuration的类)。
如果您真的需要,您也可以使用一个@ImportResource注释来加载 XML 配置文件,例如:
@Import({WebConfig.class, ServiceConfig.class})
@ImportResource("dao.xml")
这将导入WebConfig和ServiceConfig配置类以及dao.xml Spring 配置文件(参见下一章了解更多关于 XML 的内容)。
怠惰
默认情况下,Beans 是被急切地创建的——这意味着 Spring 会在启动时实例化它们并连接它们。这样可以更快地发现任何潜在的问题。如果您不希望 Bean 在必要时才加载(当使用 application context . get Bean(String)方法请求或由(例如)autowiring 请求时),可以使用@Lazy 注释使 Bean 延迟加载。
关闭应用上下文
在 web 应用中,Spring 已经优雅地关闭了 ApplicationContext。但是,在非 web 应用中,您需要注册一个关闭挂钩。
public static void main(final String[] args) throws Exception {
AbstractApplicationContext ctx
= new ClassPathXmlApplicationContext(new String []{"beans.xml"});
// add a shutdown hook for the above context...
ctx.registerShutdownHook();
// app runs here...
}
Listing 3-3App.java
这样,当应用退出时,Spring 将优雅地关闭。
BeanFactoryPostProcessors
可以实现 BeanFactoryPostProcessor 接口,以便在创建 bean(所有其他 bean)之前更改 bean 配置。例如,这对于添加定制配置很有用(尽管 Spring 自己处理大多数有用的情况)。BeanFactoryPostProcessor 接口有一个方法来定义,postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory). Spring 自动检测实现这个接口的 beans。
BeanPostProcessors
一个ApplicationContext还自动检测它接收到的实现BeanPostProcessor接口的配置元数据中定义的任何 beans。这些 bean 是特殊的,因为它们是与ApplicationContext同时创建的,并且在任何其他 bean 之前创建,因此它们可以处理其他 bean 定义。
org.springframework.beans.factory.config.BeanPostProcessor接口正好由两个回调方法组成:
Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException
我们将在后面介绍的 Spring AOP 是使用 BeanPostProcessor 接口实现的。它可以用该 bean 的代理替换每个 bean。
初始化和销毁方法
您可以在 Spring 中使用 commannotationbeanpostprocessor 来启用@ PostConstruct和@ PreDestroy这样的 JSR-250 注释。它由组件扫描激活,但也可以在 Spring 配置中直接激活。
另一种方法是使用 Spring 的内置配置。例如,Bean 注释,@Bean(initMethod = "up", destroyMethod = "down")会导致 Spring 在初始化类之后调用“up ”,并在销毁它之前注入所有依赖项和“down”。
性能
默认情况下,Spring Boot 将从名为 application.properties(对于标准属性)或 application.yml(对于 YAML 格式的属性)的文件中加载属性。
可以使用@PropertySource注释将附加属性加载到环境中。
例如,以下代码从/com/acme/目录下的类路径中加载名为 app.properties 的属性文件:
@Configuration
@PropertySource("classpath:/com/acme/app.properties")
public class AppConfig {
//configuration code...
}
Listing 3-4AppConfig.java
然后,您可以使用环境中的属性,并使用@Value注释注入它们:
@Value("${bean.name}") String beanName;
@Bean
public MyBean myBean() {
return new MyBean(beanName);
}
名为 app.properties 的文件可能具有以下值:
bean.name=Bob
这将把“Bob”注入前面提到的 beanName 字段。
环境
使用@Value 注释的替代方法是使用org.springframework.core.env.Environment类。它可以自动连接到任何类中(例如,使用@Autowired)。它有以下方法用于在运行时访问已定义的属性:
-
String getProperty(String key)–获取给定属性键的值,如果未解析,则为 null
-
String getProperty(String key,String defaultValue)-获取给定属性键的值,如果找不到,则获取给定的 default value
-
String getRequiredProperty(String key)–获取给定属性键的值,如果未找到,则抛出 IllegalStateException
轮廓
Spring 概要文件允许您配置不同的属性,甚至根据活动概要文件在运行时初始化 Beans。当将同一个应用部署到不同的环境时,例如“试运行”、“测试”和“生产”,它们会很有用您可以拥有任意数量、任意名称的配置文件。
您可以使用spring.profiles.active系统属性或 spring_profiles_active 环境变量将当前配置文件设置为活动的。您可以激活任意数量的配置文件(用逗号分隔)。
@Profile注释可以注释一个@Component bean 类(或者原型注释、@Service, @Repository和@Controller)或者一个@Bean注释的方法,甚至一个@Configuration注释的配置类。
例如,下面的配置类定义了两个不同的数据库。哪个是激活的取决于激活的配置文件。
@Configuration
public class ProfileDatabaseConfig {
@Bean("dataSource")
@Profile("development")
public DataSource embeddedDatabase() { ... }
@Bean("dataSource")
@Profile("production")
public DataSource productionDatabase() { ... }
}
Listing 3-5ProfileDatabaseConfig.java
确保为配置类中的每个@Bean 方法使用不同的名称,即使这些 Bean 被标记为不同的概要文件。否则,您可能会从 Spring 获得意外的行为,因为它使用方法名称作为 bean 名称。
拼写
什么是 Spring 表达式语言(SpEL)?Spring 表达式语言(简称 SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。
可以使用带有#{}语法的@Value注释来注入 SpEL。与只被解释为环境属性的使用${}不同,使用#{}允许您使用嵌入式语言(SpEL)的全部表达能力。
@Value("#{ T(java.lang.Math).random() * 100.0 }")
int randomNumber;
T 语法用于引用 Java 类型(前面的 java.lang.Math 类)。
您也可以使用内置变量systemProperties来引用系统属性:
@Value("#{ systemProperties['user.region'] }")
String region;
SpEL 还有 Elvis 操作符和安全导航器(很像 Kotlin、Groovy 和其他语言),例如:
@Value("#{systemProperties['pop3.port'] ?: 25}")
如果没有给pop3.port.赋值,默认为 25
您也可以使用单引号指定字符串文字,例如:
@Value("#{ 'Hello '.concat('World!') }")
String hello;
这将导致 hello 的值为“Hello World!”。
SpEL 对于 Spring Security 注释也很有用,我们将在下一章中介绍。
测试
作为 Spring-test 的一部分,spring 提供了测试支持。对于 JUnit 4 测试,您可以使用 Spring 的 SpringRunner 和@ContextConfiguration注释来指定如何为 JUnit 单元或集成测试创建 ApplicationContext,例如:
@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class})
public class MyTest {
// class body...
}
Listing 3-6MyTest.java
JUnit 5 测试与此类似,但是使用了@ExtendWith(SpringExtension.class)而不是@RunWith:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class})
public class MyTest5 {
// class body...
}
Listing 3-7MyTest5.java
编写一个包含 JUnit 测试的 Spring 应用。
四、XML 配置
Spring 配置可以通过 XML 完成。事实上,在引入 Java 配置之前,这是配置 Spring beans 的唯一方法。我们将介绍一些 Spring XML 作为参考资料,并在遗留应用中使用。
可扩展置标语言
XML 标准由基本语法、名称空间和 XML 模式定义组成。简而言之,语法是基于元素和名称的,通常是小写,用大于号和小于号包围(比如);可以在这些符号中设置并使用双引号的属性(如)。
为了清楚起见,让我们看看任何 Spring 配置 XML 文件中常见的前三行,并分析它们的含义:
-
<?xml...声明这是一个 XML 文件。 -
<beans是根元素(包装整个文档的元素),xmlns:= " ... "声明根命名空间。例如,这允许您在不指定名称空间的情况下引用。 -
xmlns:xsi=声明代表 XML 模式实例的“xsi”名称空间。这允许文档随后使用xsi:schemaLocation=来定义在哪里定位相应的 XML 模式。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
Spring XML 配置组织
为了使事情更有条理、更容易理解,在大中型应用中使用多个 XML 文件并在它们之间划分配置是有意义的。您可以决定用许多不同的方式来分离文件:水平切片(控件、服务和存储库或 Dao(数据访问对象))、垂直切片(按特性)或按功能(web 服务、前端和后端)。
XML 应用上下文
要开始使用,请使用以下应用上下文之一:
对于ClassPathXmlApplicationContext和FileSystemXmlApplicationContext,您需要指定 XML 文件。
类路径
例如,这里有一个应用入口类 App,它使用了一个ClassPathXmlApplicationContext:
package com.apress.spring_quick.di;
import com.apress.spring_quick.config.AppSpringConfig;
import com.apress.spring_quick.di.model.Message;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
final ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("classpath:/application.xml");
final MyBeanInterface myBean = applicationContext
.getBean(MyBeanInterface.class);
//...
}
}
Listing 4-1App.java
在这个例子中,"classpath:/application.xml"指的是类路径根目录下名为application. xml的文件(通常包含在例如 JAR 文件中)。在典型的构建中,您应该将这个文件放在src/main/resources/目录中,Maven 或 Gradle 会在构建过程中自动将它添加到 JAR 文件中。尽管这里我们提供了一个文件,但是也可以使用多个 XML 文件。
网
对于XmlWebApplicationContext,根上下文(可能是多个 servlet 上下文的父上下文的应用上下文)的默认位置是“/WEB-INF/applicationContext.xml”,对于名称空间为“ -servlet”的上下文,默认位置是“/WEB-INF/<name>-servlet.xml”。例如,对于 Servlet 名称为“products”的 DispatcherServlet 实例,它将查找“/WEB-INF/products-servlet.xml”。
XML Beans
在 Spring XML 中最基本的事情是创建 beans。下面是一个关于application.xml文件的例子:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class=
"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations"
value="classpath:db/datasource.properties"/>
</bean>
<bean id="dataSource1"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${db.driverClassName}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
</beans>
Listing 4-2application.xml
这个例子展示了如何定义 Spring beans 并在这些 bean 上设置属性。注意,您可以使用{db.driverClassName}如何引用db.driverClassName`属性。
初始化并销毁
initialize 方法(在 Spring 实例化并解析了 bean 上的所有依赖项之后立即调用)可以通过设置 bean 定义上的init-method属性来配置,如下面的 XML 配置所示:
<bean name="userService"
class="com.apress.spring_quick.service.UserService"
init-method="doInitialization" />
destroy 方法(在 Spring 丢弃 Spring bean 之前调用)可以通过设置destroy-method属性来配置,如下面的 XML 配置所示:
<bean name="userService"
class="com.apress.spring_quick.service.UserService"
destroy-method="doCleanup" />
当 bean 被销毁时,这可以用来删除任何不再需要的资源。这将调用如下定义的方法:
public void doCleanup() {
// do clean up
}
init-method 和 destroy-method 都应该是公共的,并将 void 作为返回类型。
启用 AOP
在 XML 中,在与应用方面的对象相同的应用上下文中使用<aop:aspectj-autoproxy>(特别是在典型的 Spring Web MVC 应用 applicationContext.xml 和...-servlet.xml)。
AOP 配置
以下示例 XML 配置使用 Spring AOP 和 Spring Retry 1 项目来重复对名为remoteCall in any class or interface ending with "Service"的方法的服务调用:
<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop = "http://www.springframework.org/schema/aop"
xsi:schemaLocation = "http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="remote"
expression="execution(* com..*Service.remoteCall(..))" />
<aop:advisor pointcut-ref="remote"
advice-ref="retryAdvice" />
</aop:config>
<bean id="retryAdvice"
class="org.springframework.retry.interceptor.RetryOperationsInterceptor"
/>
<!-- other bean definitions... -->
</beans>
注意,切入点引用了之前定义的名为“remote”的切入点。更多详情请参见第五章。
启用 Spring Data JPA
Spring Data JPA 允许您使用 ORM(对象关系映射)与数据库进行交互,比如 Hibernate 或 EclipseLink。要在 XML 中启用 Spring Data JPA,请使用以下 XML:
<beans xmlns:="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="com.acme.repositories"/>
</beans>
这将扫描“com.acme.repositories”包及其下面的任何 JPA 存储库。更多信息参见第六章。
混合 XML 和 Java 配置
没有理由不能混合使用 XML 配置和 Java 配置。事实上,您可以从 XML 激活 Java 配置,并从 Java 导入 XML 配置文件。
例如,下面的 Spring XML 文件支持 Java 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.apress.spring.config" />
</beans>
这个 XML 在“com.apress.spring.config”包和任何子包中开始组件扫描。任何标有@ Configuration、@ Component或许多其他注释的文件都会被 Spring 拾取。
从 Spring Java 配置文件中,您可以使用@ ImportResource来导入 Spring XML 文件,例如:
@Configuration
@ImportResource( { "spring-context1.xml", "spring-context2.xml" } )
public class ConfigClass { }
如果在 Spring 应用中作为配置启用(通过组件扫描或其他方式),这个类将使 Spring 读取两个文件“spring-context1.xml”和“spring-context2.xml”,作为 Spring XML 配置。
Exercise: Use Both XML and JAVA CONFIG
创建一个新的应用,同时使用 Spring XML 和 Spring Java 配置。尝试将它们以不同的方式结合起来。
Footnotes 1https://github.com/spring-projects/spring-retry
五、面向切面编程
AOP 代表面向切面编程。AOP 允许您解决横切关注点,例如日志记录、事务管理、安全性和缓存,而无需一遍又一遍地重复相同的代码。它允许你应用干(不要重复自己)原则。
Spring 在很多方面使用 AOP 本身,但也直接向开发人员公开工具。
简而言之,您通过定义切入点(添加额外特性的地方)和通知(您正在添加的特性)来使用 Spring AOP。
Spring 创建了两种类型的代理,要么是 JDK 1 (当实现一个接口时,这是内置到 JDK 中的)要么是 CGLIB 2 (当没有接口时操作字节码是必要的)。Final 类或方法不能被代理,因为它们不能被扩展。此外,由于代理实现,Spring AOP 只适用于 Spring Beans 上的公共、非静态方法。
术语
Spring AOP 使用以下术语:
-
方面——作为横切关注点的关注点的模块化。
-
连接点——它是方法执行过程中的一个点。
-
建议——一个方面在连接点采取的行动。
-
切入点——匹配一个或多个连接点的谓词称为切入点。
-
编织——向切入点添加建议的过程。
-
简介–为类型定义额外的方法字段。
-
目标对象——那些被方面建议的对象是目标对象。
-
AOP 代理 Spring AOP 创建的对象,用于满足方面契约。它在应该应用通知的地方执行通知,并委托给被代理的对象(目标对象)。
建议
有五种类型的建议(每种都有相应的注释):
-
before–在方法执行之前运行
-
after–总是在方法执行后运行,不管结果如何(类似于 Java 中的关键字
finally) -
after throwing–仅在方法引发异常时运行
-
after returning–仅在方法返回值并且可以使用该值时运行
-
around——包装方法的执行,并给出一个类型为
ProceedingJoinPoint的参数,您必须调用proceed()才能实际调用包装的方法
如何启用 AOP
您可以在配置中使用@EnableAspectJAutoProxy注释来启用 Spring AOP。
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
每个方面类都应该用@Aspect注释进行注释。在该类中,您可以指定切入点和通知。还应该用@Component对其进行注释,以便通过注释扫描拾取(或者用另一种方式配置为 Spring bean)。
如何定义切入点
您使用一个切入点表达式(由 AspectJ 项目定义的表达式的子集)来定义一个切入点。在使用注释的方面中,您在一个空方法上使用@Pointcut注释,切入点的名称就是方法的名称,例如:
@Pointcut("execution(* save(..))")
private void dataSave() {}
这里,切入点的名称是“dataSave()”。切入点方法的返回类型必须是void。它可以是任何可见度。
在这个例子中,切入点是execution(* save(..)),它指的是在任何类上执行任何名为save的方法。第一个*是通配符(匹配所有内容),指的是方法的返回类型。**..**是指方法参数,表示“零到多个参数”,而“保存”是方法名称。切入点方法本身(dataSave)不需要任何代码。通过使用切入点的名称注释另一个方法来使用切入点,例如,假设保存方法返回值或抛出异常:
@AfterReturning(value = "dataSave()", returning = "entity")
public void logSave(JoinPoint jp, Object entity) throws Throwable {
// log the entity here
}
@AfterThrowing(pointcut = "dataSave()", throwing = "ex")
public void doAfterThrowing(Exception ex) {
// you can intercept thrown exception here.
}
@Around("execution(* save(..))")
public Object aroundSave(ProceedingJoinPoint jp) throws Throwable {
return jp.proceed();
}
Listing 5-1Advice examples
如前面的方法所示,aroundSave通知也可以直接声明切入点表达式。
Spring AOP 中的切入点表达式可以使用关键字execution和within以及& &、||、和!(与,或,非)。这里还有一些例子:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz..*)")
private void inXyz() {}
@Pointcut("anyPublicOperation() && inXyz()")
private void xyzOperation() {}
Listing 5-2Pointcut examples
切入点指的是每一个公共方法。
inXyz切入点指的是 com.xyz 包中每个类的每个方法。
xyzOperation切入点结合了另外两个切入点,意味着 com.xyz 包中的每个公共方法。
释文
您也可以使用@target, @annotation或者直接使用注释在切入点表达式中指定注释,例如:
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void allRepositories() {}
// use an annotation on each actual method to advise:
@Pointcut("@annotation(com.apress.spring_quick.aop.LogMe)")
public void logMes() {}
在前面的例子中,第一个切入点应用于用@Repository注释的目标类,第二个切入点引用用@LogMe注释的方法。您也可以使用@args 来指定方法的参数具有特定的注释。
切入点表达式
Spring AOP 切入点表达式支持很多关键字。尽管其中一些表达式匹配 Spring AOP 中的相同连接点,但是它们可以用于不同的绑定(我们将在接下来讨论)。作为参考,这里有许多可能的表达方式:
| 执行(公开* *(..)) | 每个公共方法。 | | 执行(* set*(..)) | 名称以“set”开头的每个方法。 | | 执行(* com . XYZ . service . accountservice . *(..)) | AccountService 接口中定义的每个方法。 | | 执行(* com.xyz.service.*)。*(..)) | “com.xyz.service”包中每个类或接口中定义的每个非私有方法。 | | 执行(* com.xyz.service..*.*(..)) | 在“com.xyz.service”包和子包中的每个类或接口中定义的每个非私有方法。 | | 在(com.xyz.service.*)内 | “com.xyz.service”包的每个连接点。 | | 在(com.xyz.service..*) | “com.xyz.service”包和子包的每个连接点。 | | this(com . XYZ . service . accountservice) | 代理实现 com.xyz.service.AccountService 接口的每个连接点。 | | 目标(com.xyz.service.AccountService) | 目标对象实现 com.xyz.service.AccountService 接口的每个连接点。 | | args(java.io.Serializable) | 任何在运行时有一个类型为 java.io.Serializable 的参数的方法。 | | @ target(org . spring framework . transaction . annotation . transactional) | 目标对象用@Transactional 注释的任何连接点。 | | @ within(org . spring framework . transaction . annotation . transactional) | 目标对象的声明类型用@Transactional 注释的任何连接点。 | | @ annotation(org . spring framework . transaction . annotation . transactional) | 任何用@Transactional 注释的方法。 | | @ args(com . XYZ . security . my annotation) | 用 com.xyz.security.MyAnnotation 批注了单个参数的任何方法。 | | 执行(@com.xyz.security.LogMe void *(..)) | 任何用@com.xyz.security.LogMe 注释并具有`void`返回类型的方法。 |Spring AOP 中的绑定
任何通知方法都可以声明一个类型为org.aspectj.lang.JoinPoint的参数作为它的第一个参数(请注意,around advice 是要求声明类型为ProceedingJoinPoint的第一个参数,它是JoinPoint)的子接口)。JoinPoint接口提供了许多有用的方法,如下所示:
您还可以使用切入点表达式将参数传递给通知,以使用args、this、@ annotation或其他关键字链接参数,例如:
@Before("dataSave() && args(course,..)")
public void validateCourse(Course course) {
// ...
}
这有两个目的:它验证第一个参数的类型为 Course,并向 advice 方法提供该参数。
Spring AOP 的局限性
Spring AOP 只能在 Spring Beans 上建议公共的、非静态的方法。
用代理编织有一些限制。例如,如果在同一个类中有一个从一个方法到另一个方法的内部方法调用,那么对于这个内部方法调用,通知将永远不会被执行。请参见“Spring AOP 代理”图,了解这一概念的说明。
图 5-1
Spring AOP 代理
在此图中,当 method()调用类内部的 methodTwo()时,它从不调用 ProxyClass,因此不会调用 AOP 通知。然而,任何外部类(其他 Spring beans)都将调用应用任何 before、after 或 around 逻辑的代理方法。
可供选择的事物
如果需要额外的功能,可以直接使用 AspectJ。此外,像 Lombok 3 这样的项目提供了用于不同目的的字节码操作,比如创建不可变的数据类,我们将在后续章节中探讨。
Footnotes 1https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
2
https://github.com/cglib/cglib
3
六、Spring Data
Spring Data 的任务是为数据访问提供一个熟悉的、一致的基于 Spring 的编程模型,同时仍然保留底层数据存储的特性。
Spring Data 支持各种类型的数据存储,包括关系型和非关系型(SQL 和 NoSQL),从 JPA (Hibernate 或 TopLink)到 Apache Cassandra,从 MongoDB 到 Redis。在本章中,我们将探索 Spring Data JPA、JDBC 和 R2DBC,以及它们如何支持关系数据库,如 MariaDB、Oracle 或 PostgreSQL。
域实体
要开始任何 Spring Data 项目,我们需要定义项目所依赖的域类或实体。这些是映射到我们的数据库表的域对象,并且高度依赖于应用的业务域。
Spring Data 与“javax.persistence”包下的标准注释集成得很好。对于本章,假设客户实体类定义如下:
import javax.persistence.*;
import lombok.*;
@Data
@Entity
@RequiredArgsConstructor
@Table("customers")
public class Customer {
private @GeneratedValue @Id Long id;
private final String firstname;
@Column("surname")
private final String lastname;
}
为了将 Spring Data 的这个类标记为一个实体类,@Entity注释是必要的。@Id注释标记了表示表的主键的字段。像@Table和@Column这样的可选注释可以用来分别使用与字段名类别不匹配的名称。注意,我们使用 Lombok 的@Data和@RequiredArgsConstructor来消除对重复代码的需求,比如 getters、setters、toString、equals、hashCode 和 constructors。在这种情况下,因为 firstname 和 lastname 是 final,@RequiredArgsConstructor创建一个构造函数,从两个参数中设置这两个值。
此外,我们将使用以下课程:
import org.springframework.data.jpa.domain.AbstractPersistable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.NamedQuery;
@Entity
@NamedQuery(name = "Course.findByTheName",
query = "from Course c where c.name = ?1")
public class Course extends AbstractPersistable<Long> {
@Column(unique = true)
private String name;
private String subtitle;
private String description;
public Course() { this(null); }
public Course(Long id) {
this.setId(id);
}
// getters and setters omitted
}
Listing 6-1Course.java
注意,我们在这里使用@NamedQuery直接在实体类上定义定制查询。我们还使用@Column(unique = true)来指定 name 列是惟一的。此外,通过扩展AbstractPersistable<Long>,我们继承了一个 Long 类型的 Id 属性,并将该类表示为一个针对 Spring Data 的数据库持久化实体。这是可选的。
Course 和 Customer 故意以不同的方式定义,以展示在 Spring Data 中定义域实体的一些可能的方法。
这本书会经常提到拥有课程和客户的在线学习应用领域。
JDBC Spring Data
Spring Data JDBC 类似于 Spring Data JPA,建立在许多相同的抽象之上,除了 Spring Data JDBC
-
没有延迟加载
-
没有内置缓存
-
更简单
-
使用聚合根的概念
-
支持仅将查询手动定义为
@Query注释中的字符串,而不是通过方法名
聚合根是一个根实体,当您保存它时,它也保存它的所有引用,并且在编辑时,它的所有引用将被删除并重新插入。
入门指南
首先,在项目中包含 spring-data-jdbc jar 依赖项。
在 Maven 项目中,将以下内容放在依赖项下:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
或者在梯度构建中,在依赖项下包括以下内容:
implementation 'org.springframework.data:spring-data-jdbc:2.0.1.RELEASE'
使用@EnableJdbcRepositories注释使它们能够使用 Java 配置,例如,使用名为 CategoryConfiguration 的 Java 配置类:
@Configuration
@EnableJdbcRepositories
public class CategoryConfiguration {}
定义存储库
CRUD 代表“创建、读取、更新、删除”在 Spring Data 中,CrudRepository<T,ID>接口提供了与持久数据存储交互的内置方法,比如关系数据库。
要定义您的存储库,创建一个接口并用定义的通用类型扩展CrudRepository<T,ID>,其中 T 是您的实体类的类型,ID 是它的标识符的类型。Spring 将自动为您实现存储库,包括以下方法(这不是一个详尽的列表):
-
S save(S entity)-将实体保存到数据库
-
find all()–返回所有这些内容
-
S findById(ID)
-
计数()
-
删除(T)
-
existing sbyid(id)
自定义查询
当使用 Spring Data JDBC 并且您需要定制查询,或者使用 Spring Data JPA 并且内置 Spring Data 约定不能满足需求时,您可以使用@Query注释指定定制 SQL(或者当使用 Spring Data JPA 时指定 JPQL)查询,例如:
@Query("SELECT * FROM customer WHERE lastname = :lastname")
List<Customer> findAllByLastname(@Param("lastname") String lastname);
@Query("SELECT firstname, lastname FROM Customer WHERE lastname = ?1")
Customer findFirstByLastname(String lastname);
findAllByLastname 查询将按姓氏查找所有客户实体。Spring Data JDBC 只支持命名参数(如前面的:lastname),而 Spring Data JPA 也支持索引参数(如前面的?1)。@Param注释告诉 Spring Data 查询参数的名称。
Spring 支持基于
-parameters编译器标志的 Java 8 及以上版本的参数名发现。通过在您的构建中使用这个标志(Spring Boot 为您处理),您可以省略命名参数的@Param注释。
您还可以定义修改语句,如下所示:
@Query("delete from Customer c where c.active = false")
void deleteInactiveCustomers();
JPA 中的自定义查询
您还可以使用基本语法定义方法签名,Spring 将在 Spring Data JPA 中实现它们。例子如下:
-
find byx–根据一个或多个给定值查找一个实体;x 是一个条件(我们将讨论什么类型的条件是允许的)。
-
findByFirstname(字符串名称)-在本例中,Firstname 是要搜索的属性。
-
findByFirstnameAndLastname–可以使用“And”、“Or”和“Not”。
-
排序
-
findAllByX–查找符合条件的所有记录。
-
countryx
-
find topn–仅返回前 N 条记录。
情况
以下是自定义查询方法表达式中允许的条件示例:
-
支持 Is 或 Equals,但默认情况下也是隐含的。
-
IdGreaterThan(Long num) –其中 id 大于给定的 num。 -
IdLessThan(Long num) –其中 id 小于给定的 num。 -
DateLessThan(Date d) –当日期小于给定日期时,d。 -
DateGreaterThan(Date d) –当日期大于给定日期时,d -
DateBetween(Date d1, Date d2) –其中日期大于等于 d1 且小于等于 d2。 -
类似于 LessThan 和 GreaterThan 的工作,但只用于日期。
-
NameLike(String string) –其中 name 就像给定值,string。 -
NameStartingWith(String string) –其中 name 以给定值开始,string。 -
NameEndingWith(String string) –其中 name 以给定值结束,string。 -
NameContaining(String string) –其中 name 包含给定的字符串。 -
NameIgnoreCase(String string) –其中名称等于给定值,忽略大小写(不区分大小写的匹配)。 -
其中 age 匹配给定集合中的任何值。
-
AgeNotIn(Collection<Long> ages) –年龄与给定集合中的任何值都不匹配。
使用
为了更直接地连接到数据库,您可以使用org.springframework.jdbc.core.JdbcTemplate<T>。
-
这个方法接受一个 SQL 查询,任意数量的参数作为一个对象数组,以及一个为每行结果调用的回调函数。
-
这个方法与前一个方法相同,只是它采用了一个 RowMapper
<T>,将行转换成 POJO 并返回这些 POJO 的列表。
Spring Data JPA
您可以通过 Java 或 XML 启用 Spring Data 存储库代理创建,例如:
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
@Configuration
class DataConfig { //... }
Listing 6-2DataConfig.java
然后,Spring 将在运行时自动创建所有声明的存储库接口的代理实例(在 DataConfig 类的包下)。前面的@EnableJpaRepositories注释将启用 JPA 还有其他类似@ EnableMongoRepositories的口味。
要在 Spring Data 中启用 XML 格式的 JPA:
<beans xmlns:="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa
https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="com.acme.repositories"/>
</beans>
分页和排序
您可以创建一个存储库接口并扩展CrudRepository<T,ID>,Spring 将为您生成来自CrudRepository<T,ID>的内置方法的实现,以及您使用 Spring Data 的命名约定定义的自定义查询方法。
例如,下面的接口扩展了CrudRepository<T,ID>并添加了一个通过姓氏查找客户实体的方法:
@Repository
public interface PersonRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastname(String lastname);
// additional custom query methods go here
}
Listing 6-3PersonRepository.java
这里没有必要使用@Repository注释,但是您可能希望添加它来提醒每个人这个接口是代理的,并且代理是作为 Spring bean 存在的。
在CrudRepository<T,ID>之上,有一个PagingAndSortingRepository<T,ID>抽象,它添加了额外的方法来简化对实体的分页访问。它看起来像下面这样:
public interface PagingAndSortingRepository<T, ID>
extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
您还可以向自定义方法添加排序或可分页参数,以实现结果的排序和分页。
处理
事务是工作的原子单位——通常是在数据库上——它们要么完全完成,要么在出现故障时完全回滚(取消),并且可以包含任意数量的语句。Spring 可以帮助以编程方式或通过注释处理来处理事务——后者是首选。
首先,确保包含项目所需的依赖项,然后启用 Spring 的事务注释处理。请注意,在 XML 中您可以使用<tx:annotation-driven/>或者在 Java 中使用@EnableTransactionManagement来让基于注释的配置工作。
然后你可以用@ Transactional来注释一个方法(或者类)。当注释一个类时,该类的所有方法都将继承那些事务设置。它将使用您的类的 Spring 代理将每个方法包装在一个事务中。
使用代理的另一个后果是,方法只有在外部调用时才被包装在事务中。换句话说,如果一个类的一个方法直接调用同一个类中带有@ Transactional的另一个方法,它将不会调用代理,因此事务将不会被启动(或者根据注释设置进行处理)。
你也可以注释一个接口来影响每个方法;然而,Spring 团队并不建议这样做,因为只有当代理直接实现接口时,它才会起作用。
事务可以被赋予以秒为单位的超时。它们也可以标记为“只读”,并具有不同的隔离级别、传播设置和其他不同的事务设置。
例如,下面是一个带注释的查询方法定义:
@Transactional(timeout = 10, readOnly = true,
propagation = Propagation.REQUIRES_NEW)
Customer findByBirthdateAndLastname(LocalDate date, String lastname);
这将有十秒钟的超时。只读限定符为 JDBC 驱动程序提供了提示,可能会提高性能,但行为取决于驱动程序。传播被设置为 REQUIRES_NEW,这将在下面解释。
交易默认只对未勾选的异常进行回退。您可以通过设置@
Transactional批注的 rollbackFor 属性来更改这一点。
可用的不同传播设置如下:
-
REQUIRED–如果在没有事务的情况下调用该方法,则加入一个活动的事务或启动一个新的事务(这是默认行为)。
-
支持–如果存在活动事务,则加入活动事务,否则不加入事务上下文。
-
MANDATORY–如果存在活动事务,则加入活动事务;如果在没有活动事务的情况下调用该方法,则抛出异常。
-
NEVER–如果在活动事务的上下文中调用该方法,则抛出异常。
-
NOT _ SUPPORTED–挂起活动事务(如果存在)并在没有任何事务上下文的情况下执行该方法。
-
REQUIRES _ NEW–总是为此方法启动新的事务。如果使用活动事务调用该方法,则在执行该方法时,该事务将被挂起。
-
NESTED–如果在没有活动事务的情况下调用该方法,则启动一个新事务,如果在有活动事务的情况下调用该方法,则创建一个新事务,仅包装该方法的执行。
您还可以将事务的隔离级别设置为五个不同值之一(例如,使用@Transaction(isolation = Isolation.READ_COMMITTED)):
-
DEFAULT–这是默认值,取决于数据库的默认隔离级别。
-
READ _ UNCOMMITTED——这是最低级别,允许最大的并发性;但是,它会遭受脏读取、不可重复读取和幻像读取。
-
READ _ COMMITTED–这是第二低的级别,可以防止脏读,但是仍然会遭受不可重复读和幻像读。
-
REPEATABLE _ READ——这个级别防止脏读和不可重复读,代价是允许更少的并发性,但仍然会遭受幻像读。
-
SERIALIZABLE——这是最高级别的隔离,可以防止所有并发副作用,代价是非常低的并发性(一次只能发生一个可序列化的操作)。
为了理解这些隔离级别,您需要理解并发事务的挫折(脏读、不可重复读和幻像读)。脏读是指单个事务从另一个尚未提交的并发事务中读取数据。不可重复读取是指另一个事务在之前已经读取了不同的数据之后提交了新的数据。当您由于另一个事务在当前事务期间添加或删除行而获得不同的行时,会发生幻像读取。
Spring Data R2DBC
R2DBC 代表反应式关系数据库连接。它是一个 API,使用反应类型与关系数据库如 PostgreSQL、H2 和 Microsoft SQL 异步交互。
Spring Data R2DBC 1 包含了广泛的特性:
-
Spring 配置支持
-
一个带有构建器的
DatabaseClient助手接口,通过行和 POJOs 之间的集成对象映射来帮助执行常见的 R2DBC 操作 -
异常转换成 Spring 的数据访问异常
-
功能丰富的对象映射与 Spring 的转换服务相集成
-
基于注释的映射元数据,可扩展以支持其他元数据格式
-
自动实现
Repository<T,ID>接口,包括对自定义查询方法的支持
尽管该项目相对较新,但在撰写本文时,现有的驱动因素包括以下几个(带有groupId:artifactId名称):
-
posters(io . R2 DBC:R2 DBC-PostgreSQL)
-
H2 (io.r2dbc:r2dbc-h2)
-
Microsoft SQL Server(io . r2dbc:r2dbc-MSSQL)
-
MySQL (dev.miku:r2dbc-mysql)
Spring Data 有一个 R2DBC 集成,有一个 spring-boot-starter-data-r2dbc。
Spring Data R2DBC 以熟悉的方式包装 R2DBC。您可以创建一个存储库接口并扩展ReactiveCrudRepository<T,ID>,Spring 将为您生成实现。
public interface PersonRepository
extends ReactiveCrudRepository<Customer, Long> {
// additional custom query methods go here
}
与普通的CrudRepository<T,ID>不同,ReactiveCrudRepository<T,ID>方法都返回无功类型,如 Mono 和 Flux(参见第十二章了解更多关于这些类型的信息)。例如,以下是一些方法:
-
Mono<Void> delete(T entity)–从数据库中删除给定的实体 -
Flux<T> findAll()–返回该类型的所有实例 -
Mono<T> findById(org.reactivestreams.Publisher<ID> id)–按 ID 检索实体,ID 由发布者提供 -
Mono<S> save(S entity)–保存给定的实体 -
Flux<S> saveAll(Iterable<S> entities)–保存所有给定的实体 -
Flux<S> saveAll(org.reactivestreams.Publisher<S> entityStream)–保存来自给定发布者的所有给定实体
自定义反应式查询
您还可以使用@Query注释指定定制的 SQL 查询,就像 JPA 或 JDBC 一样,例如:
@Query("SELECT * FROM customer WHERE lastname = :lastname")
Flux<Customer> findByLastname(String lastname);
@Query("SELECT firstname, lastname FROM Customer WHERE lastname = ?1")
Mono<Customer> findFirstByLastname(String lastname);
科特林支架
Spring Data R2DBC 在很多方面支持 Kotlin 1.3.x。
它要求 kotlin-stdlib(或其变体之一,如 kotlin-stdlib-jdk8)和 kotlin-reflect 出现在类路径中(如果您通过 https://start.spring.io 引导 kotlin 项目,默认情况下会提供)。
有关更多信息,请参见 Spring Data R2DBC 的文档。 2
Footnotes 1https://spring.io/projects/spring-data-r2dbc
2
https://docs.spring.io/spring-data/r2dbc/docs/1.0.0.RELEASE/reference/html/#reference
七、Spring MVC
Spring web MVC 是一个用于构建 Web 服务或 Web 应用的框架,通常简称为 Spring MVC 或只是 MVC。 MVC 代表模型-视图-控制器,是 OO(面向对象)编程中常见的设计模式之一。
核心概念
Spring MVC 是按照开放-封闭原则设计的(开放用于扩展,封闭用于修改)。DispatchServlet 是 Spring MVC 的核心,它包含一个 Servlet WebApplicationContext(包含控制器、ViewResolver、HandlerMapping 和其他解析器),该 Servlet 委托给一个根 WebApplicationContext(包含应用的服务和存储库 beans)。
它检测下列类型的 beans,如果找到就使用它们;否则,将使用默认值:HandlerMapping、HandlerAdapter、HandlerExceptionResolver、ViewResolver、LocaleResolver、ThemeResolver、MultipartResolver 和 FlashMapManager。
您可以直接与视图技术(如 JSP、Velocity 或 FreeMarker)集成,也可以通过 ViewResolver 或内置的对象映射器(使用 dto(数据传输对象)或任何 POJO)返回类似 JSON 的序列化响应。
图 7-1
前端控制器
入门指南
首先,将依赖项添加到项目中。
然后使用 Java config 或 XML(或自动配置),在项目中启用 Web MVC,例如:
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.context.annotation.Configuration;
@EnableWebMvc
public class WebConfig {}
Listing 7-1WebConfig.java
还要确保使用@ComponentScan或@Bean或 XML bean 定义来定义您的 Spring Beans。在一个典型的应用中,你可能也有服务和存储库,但是对于这一章,我们将只关注 Spring Web MVC 组件。
控制器
用@Controller注释对一个类进行注释,将其标记为控制器。使用@RestController类似,但是用于 RESTful web 服务控制器——它假设每个方法的返回值都被转换成一个返回值,比如 JSON(类似于用@ResponseBody注释方法的时候)。使用这些注释中的任何一个都将允许组件扫描选择您的类。
您还可以用@RequestMapping注释该类,以设置一个将应用于该类中每个方法的 URL 前缀。例如,用@RequestMapping("/api/v1/")注释控制器类会将“/api/v1/”前缀添加到控制器中每个方法的 URL 映射中。
请求映射
用@RequestMapping或者一个相应的 HTTP 方法类型注释来注释一个方法,比如@GetMapping。每个方法请求映射应该匹配一个特定的传入请求。如果有多个方法匹配同一个 HTTP 请求,Spring 将在初始化控制器时抛出一个错误(通常在启动时)。
该类的每个方法都应使用以下内容之一进行注释,以将其映射到相应的 URL 路径:
-
@ request mapping–需要设置 HTTP 方法和路径属性,例如
@RequestMapping(method = RequestMethod.PUT, path = "/courses/{id}")。 -
@ GET mapping(
"/path")–映射到 HTTP GET。 -
@ POST mapping(
"/path")–映射到 HTTP POST。 -
@ DELETE mapping(
"/path")–映射到 HTTP DELETE。 -
@ PUT mapping(
"/path")–映射到 HTTP PUT。 -
@ PATCH mapping(
"/path")–映射到 HTTP 补丁。
您可以提供一个内嵌值的 URL 来定义可以映射到参数的路径变量。例如,在 URimg/{ filename }/raw”中,文件名对应于一个路径变量:
@GetMapping(value =img/{filename}/raw",
produces = MediaType.IMAGE_JPEG_VALUE)
public void getImage(@PathVariable String filename, OutputStream output) {
// code to send image
}
Listing 7-2Example get-mapping using path variable and produces
在本例中,给定的OutputStream参数可用于提供输出数据(本例中为图像)。您可以使用produces来设置响应的内容类型(本例中为“image/jpeg”)。
您还可以用@ ResponseStatus注释一个方法,将成功的 HTTP 状态更改为默认值(200)以外的值。例如,以下代码会将响应状态代码更改为 201:
@ResponseStatus(HttpStatus.CREATED)
@PostMapping(value = "/courses",
consumes = MediaType.APPLICATION_JSON_VALUE)
public void create(@RequestBody final CourseDto course) {
// code to save
}
Listing 7-3Create POST Mapping with custom response status
您还可以指定请求参数或头值,以使请求映射更加具体。例如,@PostMapping(value = "/courses", params = "lang=java", headers = "X-custom-header")将只匹配带有名为“lang”的查询参数、值为“java”和名为 X-custom-header 的头的 POST 请求。
路径正则表达式
还可以在路径变量定义中使用正则表达式来限制路径匹配。例如,以下内容仅匹配以数字结尾的路径:
@GetMapping("/courses/{id:\\d+}")
public CourseDto course(@PathVariable final Long id) {
// code to get Course
}
Listing 7-4Get Course by Id mapping
映射方法参数
控制器中映射方法的参数的有效注释如下:
-
@
RequestParam–一个查询参数。 -
@
PathVariable–路径的一部分。 -
@
MatrixVariable–这些变量可以出现在路径的任何部分,字符等号(" = ")用于给出值,分号(";")来限定每个矩阵变量。在同一路径上,我们还可以重复相同的变量名,或者使用逗号(“,”)字符分隔不同的值。 -
@
RequestHeader–来自请求的 HTTP 头。 -
@
CookieValue–来自 cookie 的值。 -
@
RequestPart–可用于将“multipart/form-data”请求的一部分与方法参数相关联的注释。支持的方法参数类型包括与 Spring 的MultipartResolver抽象结合的MultipartFile和与 Servlet 3.0 多部分请求结合的javax.servlet.http.Part,或者对于任何其他方法参数,部分的内容通过一个HttpMessageConverter传递,考虑请求部分的“内容类型”头。 -
@
ModelAttribute–可用于从模型中访问对象。例如,public String handleCustomer(@ModelAttribute("customer") Customer customer)将使用键"customer获得客户对象 -
@
SessionAttribute–会话的属性。 -
@
RequestAttribute–请求的属性。虽然 Java 在编译后的字节码中没有默认保留参数名,但是你可以通过一个设置来实现这一点——Spring Boot 在默认情况下会这样做,不需要任何干预,允许你自由使用与参数名关联的路径变量等等。
响应正文
用@ResponseBody注释一个方法,告诉 Spring 使用该方法的返回值作为 HTTP 响应的主体。
或者,如果用@RestController注释类,这意味着响应体是每个方法的返回值。
Spring 将使用HttpMessageConverter的实现自动将响应转换成适当的值。Spring MVC 自带内置转换器。
其他允许的响应类型有
-
实体
-
response entity——包含由 Spring 的转换逻辑序列化的实体和 HTTP 值,比如 HTTP 状态
-
HttpHeaders
-
字符串(要解析的视图的名称)
-
View
-
地图或模型
-
对象
-
DeferredResult ,Callable ,ListenableFuture ,或 CompletableFuture–异步结果
-
ResponseBodyEmitter
-
SSE 发射器
-
streamongresponsebody
-
反应型,如助焊剂
视图
Spring Web MVC 支持几种不同的视图呈现器,比如 JSP、FreeMarker、Groovy 模板和 Velocity。基于所选择的视图技术,所选择的 ViewResolver 将适当地公开模型、会话和请求属性。
Spring MVC 还包括一个 JSP 标记库来帮助构建 JSP 页面。
这里有一个总结 Spring MVC 如何工作的总体图,缺少一些细节,比如处理异常(我们将在后面讨论):
图 7-2
Spring Web MVC 请求/响应
查看解析器
Spring 提供了几种不同的视图解析器:
|视图解析器
|
描述
|
| --- | --- |
| AbstractCachingViewResolver | 缓存视图的抽象视图解析器。通常视图在使用之前需要准备;扩展此视图解析程序可提供缓存。 |
| XmlViewResolver | ViewResolver的实现,它接受用 XML 编写的配置文件,使用与 Spring 的 XML bean 工厂相同的 DTD。默认的配置文件是/WEB-INF/views.xml。 |
| ResourceBundleViewResolver | 使用由包基本名称指定的ResourceBundle中的 bean 定义的ViewResolver的实现。通常,您在位于类路径中的属性文件中定义包。默认文件名是views.properties。 |
| UrlBasedViewResolver | 简单实现了ViewResolver接口,实现了逻辑视图名称到 URL 的直接解析,没有显式的映射定义。如果您的逻辑名称以直接的方式匹配视图资源的名称,而不需要任意的映射,那么这是合适的。 |
| InternalResourceViewResolver | 支持InternalResourceView(实际上是 Servlets 和 JSP)的UrlBasedViewResolver的方便子类,以及诸如JstlView和TilesView的子类。您可以使用setViewClass(..)为这个解析器生成的所有视图指定视图类。 |
| VelocityViewResolver / FreeMarkerViewResolver / GroovyMarkupViewResolver | 分别支持VelocityView(实际上是速度模板)、FreeMarkerView或 GroovyMarkupView 的AbstractTemplateViewResolver的方便子类,以及它们的自定义子类。 |
| ContentNegotiatingViewResolver | 基于请求文件名或Accept头解析视图的ViewResolver接口的实现。 |
例如,要配置一个 Spring 应用来使用 JSP 视图,并提供来自/images 和/styles 的静态资源,您应该创建一个名为 WebConfig.java 的 Java 配置类,如下所示:
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.apress.spring_quick.web"})
public class WebConfig extends WebMvcConfigurerAdapter {
// Declare our static resources
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry {
registry.addResourceHandlerimg/**")
.addResourceLocationsimg/");
registry.addResourceHandler("/styles/**")
.addResourceLocations("/styles/");
}
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
// Serves up /WEB-INF/home.jsp for both "/" and "/home" paths:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
registry.addViewController("/home").setViewName("home");
}
@Bean
InternalResourceViewResolver getViewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/");
resolver.setSuffix(".jsp" );
resolver.setRequestContextAttribute("requestContext");
return resolver;
}
Listing 7-5WebConfig.java
这将使用 InternalResourceViewResolver 设置一个 web 应用,该应用使用。jsp”文件扩展名。
错误处理
您可以使用@ExceptionHandler注释声明一个定制的错误处理方法。当请求处理程序方法抛出任何指定的异常时,Spring 调用这个方法。
捕获的异常可以作为参数传递给方法。例如,请参见以下方法:
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public void handleArgException(IllegalArgumentException exception) {
// Log the exception
}
Listing 7-6Custom handleArgException method
注意我们如何使用@ResponseStatus将 HTTP 状态代码更改为 400(错误请求),在这种情况下返回。然而,这并不改变异常呈现的结果视图。您可以通过用@ ResponseBody注释方法并返回值来直接覆盖内容,例如:
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ExceptionDto handleArgException(IllegalArgumentException ex) {
return new ExceptionDto(ex);
}
Listing 7-7Custom handleArgException method which returns a DTO
要处理应用中所有控制器的异常,可以使用一个用@ControllerAdvice标注的类,并将所有用@ExceptionHandler标注的方法放在那里.
Web 范围
web 应用中还存在其他作用域:
-
“应用”——应用范围为 ServletContext 的生命周期创建 bean 实例,它可以跨越多个基于 servlet 的应用。
-
“请求”—请求范围为单个 HTTP 请求创建一个 bean 实例。
-
“会话”—会话作用域为 HTTP 会话创建一个 bean。
测试
Spring 通常为它的所有项目提供测试支持。因为一切都是 POJO,所以编写单元测试很简单。对于 Spring MVC,Spring Boot 提供了@WebMvcTest注释来放置一个测试类和MvcMock类型,帮助测试控制器而不会有太多的性能开销。
例如,要开始使用基于 Spring MVC 梯度构建的 Spring Boot,从下面的梯度构建文件开始:
plugins {
id 'org.springframework.boot' version '2.2.6.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.apress.spring-quick'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
Listing 7-8build.gradle
然后在与应用的主配置文件相同的包中(或在一个子包中)创建一个名为 ControllerTest 的基于 JUnit 5 的测试类,如下所示:
-
将@ExtendWith 与 SpringExtension 一起使用可以使 Spring 的测试助手扫描类似@MockBean 注释的东西,该注释创建一个 Bean,它是一个(mockito)模拟实例。
-
使用@WebMvcTest 会导致 Spring 只自动配置应用的 MVC 层,包括控制器。
-
在 MockMvc 的实例上调用 perform 会调用 HandlerMapping 逻辑,并有一个用于验证响应的 fluent 接口。在这种情况下,我们使用“
.andDo(print())”将其打印出来,然后期望 HTTP 状态为 OK (200),并使用“content().string(containsString(String))”来验证响应字符串是否具有预期的结果。
import com.apress.spring_quick.jpa.simple.Course;
import com.apress.spring_quick.jpa.simple.SimpleCourseRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ExtendWith(SpringExtension.class) //(1)
@WebMvcTest //(2)
public class ControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private SimpleCourseRepository courseRepository;
@Test
public void coursesShouldReturnAllCourses() throws Exception {
Course course = new Course();
course.setName("Java Professional");
course.setSubtitle("Java 11");
course.setDescription("");
when(courseRepository.findAll()).thenReturn(List.of(course));
mockMvc.perform(get("/api/v1/courses")) //(3)
.andDo(print()).andExpect(status().isOk())
.andExpect(content().string(
containsString("[{\"id\":null,\"title\":\"Java Professional\"" +
",\"subtitle\":\"Java 11\",\"description\":\"\"}]")));
}
}
Listing 7-9ControllerTest.java
如果没有 Spring Boot 自动配置,您也可以使用MockMvcBuilders. webAppContextSetup(WebApplicationContext)方法创建一个 MockMvc 实例,例如:
// imports:
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
// ...code:
WebApplicationContext wac = /** Create the application context */;
MockMvc mockMvc = webAppContextSetup(wac).build();
八、Spring Mobile
Spring Mobile 是 Spring MVC 的扩展,旨在简化移动 web 应用的开发。它包括一个模块,用于在服务器上检测发出请求的设备类型,是手机、平板电脑还是台式机。
入门指南
将项目包含在您的依赖项中,例如,在 Maven pom 中:
<dependency>
<groupId>org.springframework.mobile</groupId>
<artifactId>spring-mobile-device</artifactId>
<version>${org.springframework.mobile-version}</version>
</dependency>
在 Gradle 构建文件中,在“依赖项”下添加以下内容:
implementation
"org.springframework.mobile:spring-mobile-device:$mobileVersion"
然后在你的gradle.properties文件中设置版本 1 :
mobileVersion=1.1.5.RELEASE
接下来,将DeviceResolverHandlerInterceptor或DeviceResolverRequestFilter添加到您的 web 应用中。第一个与 Spring 框架的耦合更紧密,而第二个是 servlet 过滤器的实现,因此与 Spring 的耦合更少。
DeviceResolverHandlerInterceptor
Spring Mobile 配有一个HandlerInterceptor,在preHandle上,委托给一个DeviceResolver。被解析的Device被设置为一个名为currentDevice的请求属性,使它在整个请求处理过程中对处理程序可用。
要启用它,请将DeviceResolverHandlerInterceptor添加到您的DispatcherServlet配置 XML 中定义的拦截器列表中:
<interceptors>
<bean class="org.springframework.mobile.device.DeviceResolverHandlerInterceptor" />
</interceptors>
或者,您可以使用 Spring 的基于 Java 的配置来添加DeviceResolverHandlerInterceptor:
@Configuration
@EnableWebMvc
@ComponentScan
public class WebConfig implements WebMvcConfigurer {
//...
@Bean
public DeviceResolverHandlerInterceptor drhInterceptor() {
return new DeviceResolverHandlerInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(drhInterceptor());
}
}
DeviceResolverRequestFilter
作为DeviceResolverHandlerInterceptor的替代,Spring Mobile 还附带了一个 servlet 过滤器,它委托给一个DeviceResolver。与HandlerInterceptor一样,被解析的Device被设置在一个名为currentDevice”的请求属性下。
要启用,请将DeviceResolverRequestFilter添加到您的 web.xml,如下所示:
<filter>
<filter-name>deviceResolverRequestFilter</filter-name>
<filter-class>
org.springframework.mobile.device.DeviceResolverRequestFilter
</filter-class>
</filter>
访问设备
要在代码中查找当前的Device,可以用几种方法。如果您已经引用了一个ServletRequest或 SpringWebRequest,只需使用DeviceUtils:
//imports
import org.springframework.mobile.device.DeviceUtils;
// code...
Device currentDevice = DeviceUtils.getCurrentDevice(servletRequest);
这将获得当前设备,如果没有为请求解析设备,则为 null。如果当前设备没有被解析,还有一个getRequiredCurrentDevice(HttpServletRequest request)方法抛出运行时异常。
设备接口有以下可用方法:
|返回类型
|
方法
|
| --- | --- |
| DevicePlatform | getDevicePlatform()–返回一个枚举,可以是 IOS、ANDROID 或 UNKNOWN。 |
| Boolean | isMobile()–如果该设备是苹果 iPhone 或 Nexus One Android 等移动设备,则为 True。 |
| Boolean | isNormal()–如果该设备不是移动或平板设备,则为 True。 |
| Boolean | isTablet()–如果该设备是苹果 iPad 或摩托罗拉 Xoom 等平板设备,则为 True。 |
DeviceWebArgumentResolver
如果您想将当前的Device automatically作为参数传递给一个或多个控制器方法,请配置一个DeviceWebArgumentResolver using XML:
<annotation-driven>
<argument-resolvers>
<bean class="org.springframework.mobile.device.DeviceWebArgumentResolver" />
</argument-resolvers>
</annotation-driven>
您也可以使用基于 Java 的配置来配置DeviceHandlerMethodArgumentResolver,如下所示:
@Bean
public DeviceHandlerMethodArgumentResolver deviceHMAR() {
return new DeviceHandlerMethodArgumentResolver();
}
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(deviceHMAR());
}
LiteDeviceResolver
Spring 允许不同的DeviceResolver实现,但是默认情况下只提供一个名为LiteDeviceResolver的移动或平板设备。
您还可以通过添加额外的关键字来自定义LiteDeviceResolver,如果这些关键字包含在请求的用户代理中,将被解析为“普通”设备,例如,使用 Java 配置:
@Bean
public LiteDeviceResolver liteDeviceResolver() {
List<String> keywords = new ArrayList<String>();
keywords.add("vivaldi");
keywords.add("yandex");
return new LiteDeviceResolver(keywords);
}
@Bean
public DeviceResolverHandlerInterceptor deviceResolverHandlerInt() {
return new DeviceResolverHandlerInterceptor(liteDeviceResolver());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(deviceResolverHandlerInt());
}
站点偏好管理
Spring Mobile 提供了一个名为StandardSitePreferenceHandler的单一SitePreferenceHandler实现,这应该适合大多数需求。它支持基于查询参数的站点偏好指示(site_preference)和可插拔的SitePreference存储,并且可以在使用the SitePreferenceHandlerInterceptor的 Spring MVC 应用中启用。此外,如果用户没有明确指示SitePreference,将基于检测到的用户设备导出默认值。
因此,除了前面的拦截器之外,还要添加以下内容:
@Bean
public SitePreferenceHandlerInterceptor
sitePreferenceHandlerInterceptor() {
return new SitePreferenceHandlerInterceptor();
}
然后将addInterceptors方法更新如下:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(drhInterceptor());
registry.addInterceptor(sitePreferenceHandlerInterceptor());
}
与设备分辨率类似,您可以使用SitePreferenceUtils或SitePreferenceHandlerMethodArgumentResolver来访问当前的SitePreference。然而,将移动用户重定向到不同的站点可能更有意义。在这种情况下,您可以使用SiteSwitcherHandlerInterceptor将移动用户重定向到专用的移动站点。
SitePreferenceHandlerInterceptor的mDot、dotMobi, urlPath, and standard工厂方法配置基于 cookie 的SitePreference存储。cookie 值将在移动和普通站点域之间共享。在内部,拦截器委托给一个SitePreferenceHandler,所以在使用切换器时不需要注册一个SitePreferenceHandlerInterceptor。例如,以下拦截器会将移动用户重定向到 mobile.app.com,将平板电脑重定向到 tablet.app.com,否则只重定向 app.com:
@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
return SiteSwitcherHandlerInterceptor.standard("app.com",
"mobile.app.com", "tablet.app.com", ".app.com");
}
// standard(normalName, mobileServerName, tabletServerName, cookieDomain)
一种不需要额外 DNS 条目的更简单的方法是 urlPath 工厂:
@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
return SiteSwitcherHandlerInterceptor.urlPath("/mobile");
}
这个拦截器会将移动用户重定向到 /mobile/ paths。例如,如果正常的 URL 是“myapp.com/courses”,那么移动站点将是“myapp.com/mobile/courses”。
Spring 移动示例
这个例子将基于前几章的 Spring Web MVC 和 Spring Data 内容,并利用 Spring Boot。有关这两个主题的更多信息,请参见相关章节。这个示例项目可以在网上找到。 2
首先,创建一个名为“spring-mobile”的新目录,并创建一个 Gradle“build . Gradle”文件,如下所示:
plugins {
id 'org.springframework.boot' version '2.3.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id "java"
}
group = 'com.apress.spring-quick'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
ext {
mobileVersion = '1.1.5.RELEASE'
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter-actuator"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-groovy-templates"
implementation "org.springframework.mobile:spring-mobile-device:$mobileVersion"
implementation "com.apress.spring-quick:spring-data-jpa:0.0.1"
implementation "com.h2database:h2:1.4.192" // database
}
Listing 8-1build.gradle
这使用了 Spring Boot Gradle 插件和依赖管理来简化项目的设置。注意,我们包含了第六章中的“spring-data-jpa”项目作为依赖项。这使得存储库可以在运行时作为 Spring beans 包含(取决于配置)。
接下来,创建一个主类,如下所示:
@SpringBootApplication
@Import({WebConfig.class, ServiceConfig.class})
public class SpringMobileWebApp {
public static void main(String[] args) throws IOException {
SpringApplication.run(SpringMobileWebApp.class, args);
}
}
Listing 8-2SpringMobileWebApp.java
接下来,设置 ServiceConfig,它包括来自第六章的“spring-data-jpa”项目的特定包:
@Configuration
@EnableJpaRepositories(basePackages =
{"com.apress.spring_quick.jpa.simple", "com.apress.spring_quick.jpa.compositions"},
enableDefaultTransactions = true)
@ComponentScan(basePackages = {"com.apress.spring_quick.jpa.simple", "com.apress.spring_quick.jpa.compositions"})
public class ServiceConfig {
}
接下来,我们指定 WebConfig 类,该类定义了本章前面描述的拦截器,以及 GroovyMarkupConfigurer 和 GroovyMarkupViewResolver:
import org.springframework.context.annotation.*;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.mobile.device.*;
import org.springframework.mobile.device.site.*;
import org.springframework.mobile.device.switcher.*;
import org.springframework.web.method.support.*;
@Configuration
@EnableWebMvc
@ComponentScan
public class WebConfig implements WebMvcConfigurer {
@Bean
public SitePreferenceHandlerMethodArgumentResolver sitePrefMAR() {
return new SitePreferenceHandlerMethodArgumentResolver();
}
@Bean
public DeviceHandlerMethodArgumentResolver deviceHMAR() {
return new DeviceHandlerMethodArgumentResolver();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(deviceHMAR());
argumentResolvers.add(sitePrefMAR());
}
@Bean
public DeviceResolverHandlerInterceptor drhInterceptor() {
return new DeviceResolverHandlerInterceptor();
}
@Bean
public SitePreferenceHandlerInterceptor sitePreferenceHandlerInterceptor() {
return new SitePreferenceHandlerInterceptor();
}
@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor(){
return SiteSwitcherHandlerInterceptor.urlPath("/mobile");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(drhInterceptor());
registry.addInterceptor(sitePreferenceHandlerInterceptor());
registry.addInterceptor(siteSwitcherHandlerInterceptor());
}
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("classpath:/templates/");
return configurer;
}
@Bean
public GroovyMarkupViewResolver groovyMarkupViewResolver() {
GroovyMarkupViewResolver resolver = new GroovyMarkupViewResolver();
resolver.setSuffix(".groovy");
resolver.setRequestContextAttribute("requestContext");
return resolver;
}
}
Listing 8-3WebConfig.java
注意,我们已经使用“/mobile”路径定义了一个SiteSwitcherHandlerInterceptor。Groovy 相关的配置告诉 Spring 在类路径中的/templates/下查找以“.”结尾的文件。太棒了。
最后,我们需要为 MVC 应用定义控制器。为了给移动站点启用完全不同的逻辑,我们可以为移动和普通请求定义一个单独的控制器。或者,我们可以将SitePreference作为方法参数注入到每个控制器方法中,并使用它,因为我们设置了一个SitePreferenceHandlerMethodArgumentResolver。
在本例中,我们创建了一个 CourseController 和 MobileCourseController,如下所示:
@Controller
@RequestMapping("/mobile")
public class MobileCourseController {
@GetMapping("/")
public String home() {
return "mobile/home";
}
// additional methods...
Listing 8-5MobileCourseController.java
@Controller
@RequestMapping
public class CourseController {
@GetMapping("/")
public String home() {
return "home";
}
// additional methods...
Listing 8-4CourseController.java
请注意,由于 mobileCourseController 是用@RequestMapping("/mobile ")注释的,它将匹配所有以"/mobile "开头的路径,因此匹配所有 SitePreference 为 Mobile 的用户。同样,我们也可以对平板电脑做同样的事情。
Groovy 标记模板应该放在 src/main/resources/templates 目录下。“home.groovy”模板应该如下所示:
yieldUnescaped '<!DOCTYPE html>'
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content: "text/html; charset: utf-8"')
title('Courses Demo')
link(rel: 'stylesheet', href: '/styles/main.css', type: 'text/css')
}
body {
h3('Normal Home page')
div(class: 'site_pref') {
a(href: '/?site_preference=mobile', 'Mobile')
yieldUnescaped '|'
a(href: '/?site_preference=normal', 'Desktop')
}
div(class: 'content') {
div {
a(href: '/courses', 'Courses')
}
}
}
}
使用 URL ?site_preference=mobile (或者点击具有相同 URL 路径的网页上的“移动”链接)触发 SiteSwitcherHandlerInterceptor 来改变 SitePreference。在这种情况下,用户将被重定向到由文件src/main/resources/templates/mobile/home.groovy呈现的“移动/家庭”视图。
图 8-1
移动/普通主页
Exercise: Add Tablets
从本章的代码开始(可在网上获得 3 ),添加对平板电脑的支持。
Footnotes 1在撰写本文时,最新的里程碑版本是 2.0.0.M3,所以在您阅读本文时,2.0.0 可能已经发布了。
2
https://github.com/adamldavis/spring-quick-ref
3
https://github.com/adamldavis/spring-quick-ref