Spring实战-第四版学习



JVM调优

参数 含义
-Xms2G -Xmx2G 代表jvm可用的heap内存最小和最大
-XX:PermSize -XX:MaxPermSize 代表jvm的metadata内存的大小

1、启动报的错是:Could not reserve enough space for object heap error

1
-Xms512M -Xmx1024M

2、Jvm out of memroy 报错总结:

  • Java heap space: 增加-xmx
  • PermGen space: 增加-XX:PermSize
  • Requested array size exceeds VM limit: 错误的意思是创建数组的大小超过了heap的最大大小,所以解决办法就是,要么增加-xmx,要么减小要创建的这个数组大小。

简化Java开发

依赖注入

任何一个有实际意义的应用(肯定比 Hello World 示例更复杂)都会由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻辑。按照传统的做法,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。

通过 DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,如图 1.1 所示,依赖关系将被自动注入到需要它们的对象当中去。

为了展示这一点,让我们看一看以下的 BraveKnight,这个骑士不仅勇敢,而且能挑战任何形式的探险:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BraveKnight implements Knight {

private Quest quest;

public BraveKnight(Quest quest) {
this.quest = quest;
}

public void embarkOnQuest() {
quest.embark();
}

}

BraveKnight 没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入

这里的要点是 BraveKnight 没有与任何特定的 Quest 实现发生耦合。对它来说,被要求挑战的探险任务只要实现了 Quest 接口,那么具体是哪种类型的探险就无关紧要了。这就是 DI 所带来的最大收益 — 松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。

创建应用组件之间协作的行为通常称为装配(wiring)。Spring 有多种装配 bean 的方式,采用 XML 是很常见的一种装配方式。以下是一个简单的 Spring 配置文件:knights.xml,该配置文件将 BraveKnight、SlayDragonQuest装配到了 一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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 id="knight" class="sia.knights.BraveKnight">
<constructor-arg ref="quest" />
</bean>

<bean id="quest" class="sia.knights.SlayDragonQuest"/>

</beans>

如果 XML 配置不符合你的喜好的话,Spring 还支持使用 Java 来描述配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class KnightConfig {

@Bean
public Knight knight() {
return new BraveKnight(quest());
}

@Bean
public Quest quest() {
return new SlayDragonQuest();
}

}

不管你使用的是基于 XML 的配置还是基于 Java 的配置,DI 所带来的收益都是相同的。现在已经声明了 BraveKnight 和 Quest 的关系,接下来我们只需要装载 XML 配置文件,并把应用启动起来。

观察它如何工作:

1
2
3
4
5
6
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("META-INF/spring/knight.xml");
BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath:spring-config.xml");

Knight knight = applicationContext.getBean(Knight.class);
knight.embarkOnQuest();
applicationContext.close();

这里的 main() 方法基于 knights.xml 文件创建了 Spring 应用上下文。随后它调用该应用上下文获取一个 ID 为 knight 的 bean。得到Knight 对象的引用后,只需简单调用 embarkOnQuest() 方法就可以执行所赋予的探险任务了。注意这个类完全不知道我们的英雄骑士接受哪种探险任务,而且完全没有意识到这是由 BraveKnight 来执行的。只有 knights.xml 文件知道哪个骑士执行哪种探险任务。

注入方式

  1. 注解注入
1
2
@Autowired
private CDPlayerService cdPlayerService;
  1. 构造器注入
1
2
3
4
5
6
private CDPlayerService cdPlayerService;

@Autowired
public CDPlayerServiceTest(CDPlayerService cdPlayerService) {
this.cdPlayerService = cdPlayerService;
}
  1. setter方法注入
1
2
3
4
5
6
private CDPlayerService cdPlayerService;

@Autowired
public void setCdPlayerService(CDPlayerService cdPlayerService) {
this.cdPlayerService = cdPlayerService;
}

应用切面

DI 能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。

AOP 应用:

每一个人都熟知骑士所做的任何事情,这是因为吟游诗人用诗歌记载了骑士的事迹并将其进行传唱。假设我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。程序清单 1.10 展示了我们会使用的 Minstrel 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BraveKnight implements Knight {

private Quest quest;
private Minstrel minstrel;

public BraveKnight(Quest quest, Minstrel minstrel) {
this.quest = quest;
this.minstrel = minstrel;
}

public void embarkOnQuest() throws QuestException {
minstrel.singBeforeQuest();
quest.embark();
minstrl.singAfterQuest();
}

}

简单的 BraveKnight 类开始变得复杂,如果你还需要应对没有吟游诗人时的场景,那代码会变得更复杂。但利用 AOP,你可以声明吟游诗人必须歌颂骑士的探险事迹,而骑士本身并不用直接访问 Minstrel 的方法。

要将 Minstrel 抽象为一个切面,你所需要做的事情就是在一个 Spring 配置文件中声明它。程序清单 1.11 是更新后的 knights.xml 文件,Minstrel 被声明为一个切面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="knight" class="sia.knights.BraveKnight">
<constructor-arg ref="quest" />
</bean>

<bean id="quest" class="sia.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>

<!-- 声明为一个切面:吟游诗人必须歌颂骑士的探险事迹 -->
<bean id="minstrel" class="sia.knights.Minstrel">
<constructor-arg value="#{T(System).out}" />
</bean>

<aop:config>
<aop:aspect ref="minstrel">
<aop:pointcut id="embark"
expression="execution(* *.embarkOnQuest(..))"/>

<aop:before pointcut-ref="embark"
method="singBeforeQuest"/>

<aop:after pointcut-ref="embark"
method="singAfterQuest"/>
</aop:aspect>
</aop:config>

</beans>

这里使用了 Spring 的 aop 配置命名空间把 Minstrel bean 声明为一个切面。首先,需要把 Minstrel 声明为一个 Spring bean,然后在元素中引用该 bean。

容纳你的 Bean

在基于 Spring 的应用中,你的应用对象生存于 Spring 容器(container) 中。如图 1.4 所示,Spring 容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期,从生存到死亡(在这里,可能就是 new 到 finalize())。

容器是 Spring 框架的核心。Spring 容器使用 DI 管理构成应用的组件,它会创建相互协作的组件之间的关联。毫无疑问,这些对象更简单干净,更易于理解,更易于重用并且更易于进行单元测试。

Spring 自带了多个容器实现,可以归为两种不同的类型。bean 工厂(由 org.springframework.beans.factory.BeanFactory 接口定义)是最简单的容器,提供基本的 DI 支持。应用上下文(由 org.springframework.context.ApplicationContext 接口定义)基于 BeanFactory 构建,并提供应用框架级别的服务,例如从属性文件解析文本信息以及发布应用事件给感兴趣的事件监听者。

使用应用上下文

Spring 自带了多种类型的应用上下文。下面罗列的几个是你最有可能遇到的。

  • AnnotationConfigApplicationContext:从一个或多个基于 Java 的配置类中加载 Spring 应用上下文。
  • AnnotationConfigWebApplicationContext:从一个或多个基于 Java 的配置类中加载 Spring Web 应用上下文。
  • ClassPathXmlApplicationContext:从类路径下的一个或多个 XML 配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
  • FileSystemXmlapplicationcontext:从文件系统下的一 个或多个 XML 配置文件中加载上下文定义。
  • XmlWebApplicationContext:从 Web 应用下的一个或多个 XML 配置文件中加载上下文定义。

无论是从文件系统中装载应用上下文还是从类路径下装载应用上下文,将 bean 加载到 bean 工厂的过程都是相似的。例如,如下代码展示 了如何加载一个 FileSystemXmlApplicationContext:

1
2
3
4
5
6
// 在指定的文件系统路径下查找 knight.xml文件
ApplicationContext context = new FileSystemXmlApplicationContext("c:/knight.xml");
// 在所有的类路径(包含 JAR 文件)下查找 knight.xml 文件
ApplicationContext context = new ClassPathXmlApplicationContext("knight.xml");
// 通过一个配置类加载 bean
ApplicationContext context = new AnnotationConfigApplicationContext(cn.lauy.config.KnightConfig.class);

应用上下文准备就绪之后,我们就可以调用上下文的 getBean() 方法从 Spring 容器中获取 bean。

bean 的生命周期

Spring 容器中的 bean 的生命周期就显得相对复杂多了。正确理解 Spring bean 的生命周期非常重要,因为你或许要利用 Spring 提供的扩展点来自定义 bean 的创建过程。图 1.5 展示了 bean 装载到 Spring 应用上下文中的一个典型的生命周期过程。

我们对图 1.5 进行详细描述:

  1. Spring 对 bean 进行实例化;
  2. Spring 将值和 bean 的引用注入到 bean 对应的属性中;
  3. 如果 bean 实现了 BeanNameAware 接口,Spring 将 bean 的 ID 传递给 setBeanName()方法;
  4. 如果 bean 实现了 BeanFactoryAware 接口,Spring 将调用 setBeanFactory() 方法,将 BeanFactory 容器实例传入;
  5. 如果 bean 实现了 ApplicationContextAware 接口,Spring 将调用 setApplicationContext() 方法,将 bean 所在的应用上下文的引用传入进来;
  6. 如果 bean 实现了 BeanPostProcessor 接口,Spring 将调用它们的 postProcessBefore-Initialization() 方法;
  7. 如果 bean 实现了 InitializingBean 接口,Spring 将调用它们的 afterPropertiesSet() 方法。类似地,如果 bean 使用 initmethod 声明了初始化方法,该方法也会被调用;
  8. 如果 bean 实现了 BeanPostProcessor 接口,Spring 将调用它们的 postProcessAfter-Initialization() 方法;
  9. 此时,bean 已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
  10. 如果 bean 实现了 DisposableBean 接口,Spring 将调用它的 destroy() 接口方法。

同样,如果 bean 使用 destroy-method 声明了销毁方法,该方法也会被调用。现在你已经了解了如何创建和加载一个 Spring 容器。但是一个空的容器并没有太大的价值,在你把东西放进去之前,它里面什么都没有。为了从 Spring 的 DI 中受益,我们必须将应用对象装配进 Spring 容器中。我们将在第 2 章对 bean 装配进行更详细的探讨。

详细参考-我的笔记Spring Bean的生命周期

装配 Bean

自动化装配 bean

Spring 从两个角度来实现自动化装配:

  • 组件扫描(component scanning):Spring 会自动发现应用上下文中所创建的 bean。
  • 自动装配(autowiring):Spring 自动满足 bean 之间的依赖。

创建可被发现的 bean

为了在 Spring 中阐述这个例子,让我们首先在 Java 中建立 CD播放器 的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface CDPlayerService {
void play();
}

// 如果想为这个 bean 设置不同的 ID,你所要做的就是将期望的 ID 作为值传递给 @Component 注解
// 比如 @Component("cDPlayerServiceImpl")
@Component
public class CDPlayerServiceImpl implements CDPlayerService {
private String title = "Sgt. Pepper's Lonely Hearts Club Band";
private String artist = "The Beatles";

@Override
public void play() {
System.out.println("Playing " + title + " by " + artist);
}

}

// @ComponentScan 还提供了另外一种方法,那就是将其指定为包中所包含的类或接口:
// 比如 @ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.clas})
@ComponentScan(basePackages = "cn.lauy") // 设置组件扫描的基础包
@Configuration
public class CompentConfig {
}

其中 ComponentScan 扫描 同样可以通过 xml 配置

1
2
3
4
5
6
7
8
9
10
11
12
<?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"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
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="cn.lauy" />

</beans>

然后我们可以通过单元测试得知 CDPlayerService 被创建出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.lauy.service;

import cn.lauy.MainApplication;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest(classes = MainApplication.class) // MainApplication 自定义的启动类入口
class CDPlayerServiceTest {

@Autowired
private CDPlayerService cdPlayerService;

@Test
public void cdShouldNotBeNull() {
assertNotNull(cdPlayerService);
}
}

通过Java代码装配 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
public class CDPlayerConfig {

/**
* 默认情况下,bean 的 ID 与带有 @Bean 注解的方法名是一样的
*/
@Bean
public CDPlayerService cdPlayerService() {
return new CDPlayerServiceImpl();
}

/**
* 如果你想为其设置成一个不同的名字的话,那么可以重命名该方法,也可以通过 name 属性指定一个不同的名字。
*/
@Bean("cDPlayerServiceName")
public CDPlayerService cdPlayerServiceSpecifyName() {
return new CDPlayerServiceImpl();
}

/**
* cdPlayer() 方法请求方法 cdPlayerService 作为参数
*/
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(cdPlayerService());
}

/* 没有装配 */
public CDPlayer cdPlayer123() {
return new CDPlayer(cdPlayerService());
}
}

然后我们通过如下代码获取Spring上下文中的Bean信息

1
2
3
ApplicationContext context = new AnnotationConfigApplicationContext(cn.lauy.config.CDPlayerConfig.class);

CDPlayer cdPlayer = (CDPlayer)context.getBean("cdPlayer");

可以知道,成功注入了名叫 cdPlayer的CDPlayer对象 、 cdPlayerService的CDPlayerService对象 和 指定了名称的 cdPlayerServiceSpecifyName的CDPlayerService对象

导入配置

现在,我们临时假设 CDPlayerConfig 已经变得有些笨重,我们想要将其进行拆分。当然,它目前只定义了两个 bean,远远称不上复杂的 Spring 配置。不过,我们假设两个 bean 就已经太多了。

我们所能实现的一种方案就是将 BlankDisc 从 CDPlayerConfig 拆分出来,定义到它自己的 CDConfig 类中,如下所示:

1、加载另外的JavaConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@ComponentScan(basePackages = "cn.lauy")
@Import(CDConfig.class)
@Configuration
public class CDPlayerConfig {

@Bean
public CDPlayerService cdPlayerService() {
return new CDPlayerServiceImpl();
}

}

// 通过 @Import 注解将此配置类加载进去
public class CDConfig {

@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(new CDPlayerService() {
@Override
public void play() {

}
});
}

}

2、在Javaconfig中引用XML配置

先在resource根目录创建文件 LoadBeanConfig.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--spring启动报错:schemalocation必须有偶数个URI, 注意 xsi:schemaLocation 个数-->

<!-- public UserInfo(String id, String userName) { } -->
<bean id="userInfo" class="cn.lauy.bean.UserInfo">
<constructor-arg name="id" value="woaini" type="java.lang.String"/>
<constructor-arg name="userName" value="name" type="java.lang.String"/>
</bean>

<bean id="person" class="cn.lauy.bean.Person">
<!-- 设置属性:需要在Person类中配置 setId方法 -->
<property name="id" value="201614100130" />
<constructor-arg ref="userInfo"/>
</bean>

<!-- 我们使用了 `c-` 命名空间来声明构造器参数,它作为元素的一个属性, c:cd-ref="userInfo" -->
<bean id="personInfo" class="cn.lauy.bean.Person" c:_0-ref="userInfo"/>

<!--启用组件扫描 或者 使用注解 @ComponentScan-->
<context:component-scan base-package="cn.lauy"/>
</beans>

图 2.1 描述了这个属性名是如何组合而成的

然后通过 Javaconfig 配置类引入

1
2
3
4
5
6
7
@ComponentScan(basePackages = "cn.lauy")
@Import(CDConfig.class)
@ImportResource("classpath:LoadBeanConfig.xml")
@Configuration
public class CDPlayerConfig {

}

以上在 JavaConfig 配置中,我们已经展现了如何使用 @Import 和 @ImportResource 来拆分 JavaConfig 类。在 XML 中,我们可以使 用 import 元素来拆分 XML 配置。

3、在XML配置中引用Javaconfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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:c="http://www.springframework.org/schema/c"
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 https://www.springframework.org/schema/context/spring-context.xsd">

<!--导入Javaconfig-->
<bean class="cn.lauy.config.CDConfig" />

<!--导入其他xml配置文件-->
<import resource="CDPlayerConfig.xml"/>

<!--启用组件扫描:如果关闭会导致下面Result的很多组件无法被扫描到-->
<context:component-scan base-package="cn.lauy"/>
</beans>

在这里插入图片描述

在本章中,我们看到了在 Spring 中装配 bean 的三种主要方式:自动化配置、基于 Java 的显式配置以及基于 XML 的显式配置。不管你采用什么方式,这些技术都描述了 Spring 应用中的组件以及这些组件之间的关系。

高级装配

配置profile bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@ActiveProfiles("dev") // 激活配置文件
public class DataSourceConfig {

@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}

@Bean
@Profile("prod")
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}

}

这边我们引伸下,在Javaconfig中设置spring的启动端口号

1
2
3
4
5
6
7
8
9
10
@Configuration
public class ServerPortConfig implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {

// @Autowired 这个也可以不要// 使用@Bean注解找不到,可能是方法重写有影响
@Override
public void customize(ConfigurableWebServerFactory factory) {
factory.setPort(8099);
}

}

条件化的bean

Spring 4 引入了一个新的 @Conditional 注解,它可以用到带有 @Bean 注解的方法上。如果给定的条件计算结果为 true,就会创建这个bean,否则的话,这个bean会被忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
// 继承接口Condition
Class<? extends Condition>[] value();

}

@FunctionalInterface
public interface Condition {

boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

这个接口实现起来很简单直接,只需提供 matches() 方法的实现即可。如果 matches() 方法返回 true,那么就会创建带有 @Conditional 注解的 bean。如果 matches() 方法返回 false,将不会创建这些 bean。

1
2
3
4
5
6
7
8
9
public class MyCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
CDPlayer cdPlayer = (CDPlayer)context.getBeanFactory().getBean("cdPlayer");
return env.containsProperty("magic") || cdPlayer != null;
}
}

使用

1
2
3
4
5
@Conditional(MyCondition.class) // 实现Condition接口的类
@Bean("cDPlayerServiceName")
public CDPlayerService cdPlayerServiceSpecifyName() {
return new CDPlayerServiceImpl();
}

处理自动装配的歧义

在本例中,CDPlayerService是一个接口,并且有三个类实现了这个接口,分别为 CDPlayerServiceImpl、CDPlayerGoodServiceImpl:

1、标示首选的 bean

1
2
3
4
@Autowired
public void setCDPlayerService(CDPlayerService cDPlayerService) {
this.cDPlayerService = cDPlayerService;
}
1
2
3
4
@Component
public class CDPlayerGoodServiceImpl implements CDPlayerService { ... }
@Component
public class CDPlayerServiceImpl implements CDPlayerService { ... }

因为这三个实现均使用了 @Component 注解,在组件扫描的时候,能够发现它们并将其创建为 Spring 应用上下文里面的 bean。然后,当 Spring 试图自动装配 setCDPlayerService() 中的 CDPlayerService参数时,它并没有唯一、无歧义的可选值。

当然,我们首先可以考虑使用 @Bean 装配,然后换成对应的装配名称 cdPlayerService。

1
2
3
4
@Bean
public CDPlayerService cdPlayerService() {
return new CDPlayerServiceImpl();
}

或者我们可以使用 Spring 注解标志首选的 Bean,但是这种有很大局限性。

1
2
3
4
5
@Component
@Primary // 标志为首选的Bean
public class CDPlayerServiceImpl implements CDPlayerService {
...
}

2、限定自动装配的bean

1
2
3
4
5
6
7
8
9
10
11
12
13
@Qualifier("cold")
@Autowired
private CDPlayerService cdPlayerService1;

@Component
@Qualifier("cold") // 自定义名,推荐
public class CDPlayerGoodServiceImpl implements CDPlayerService {

// ==> 等价于
@Bean("cold")
public CDPlayerService cdPlayerGoodService() {
return new CDPlayerGoodServiceImpl();
}

这是使用限定符的最简单的例子。为 @Qualifier 注解所设置的参数就是想要注入的 bean 的 ID。所有使用 @Component 注解声明的类都会创建为 bean,并且 bean 的 ID 为首字母变为小写的类名。因此,@Qualifier(“cold”) 指向的是组件扫描时所创建的 bean,并且这个 bean 是 CdPlayerGoodService类的实例。

当然我们也可以自定义注解,比如 @Cold

1
2
3
4
5
6
7
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD,
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
String value() default "";
}

然后像 @Qualifier 一样使用即可。

bean 的作用域

Spring 定义了多种作用域,可以基于这些作用域创建 bean,包括:

  • 单例(Singleton):在整个应用中,只创建 bean 的一个实例。
  • 原型(Prototype):每次注入或者通过 Spring 应用上下文获取的时候,都会创建一个新的 bean 实例。
  • 会话(Session):在 Web 应用中,为每个会话创建一个 bean 实例。
  • 请求(Rquest):在 Web 应用中,为每个请求创建一个 bean 实例。

单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其他的作用域,要使用 @Scope 注解,它可以与 @Component 或 @Bean 一起使用。

这里,使用 ConfigurableBeanFactory 类的 SCOPE_PROTOTYPE 常量设置了原型作用域。你当然也可以使用 @Scope(“prototype”),但是使用 SCOPE_PROTOTYPE 常量更加安全并且不易出错。

1
2
3
4
5
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 设置了原型作用域
public CDPlayer cdPlayer() {
return new CDPlayer(() -> "new CDPlayerService()");
}

运行时注入值

1、通过 @PropertySource 注入外部的值

在 Spring 中,处理外部值的最简单方式就是声明属性源并通过 Spring 的 Environment 来检索属性。

LoadBeanFromOuter.properties文件

1
2
user.id=The Beatles
user.userName=Sgt. Peppers Lonely Hearts Club
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@PropertySource("classpath:LoadBeanFromOuter.properties")
public class ExpressiveConfig {

@Autowired
Environment env;

@Bean
public UserInfo loadUserInfo() {
env.getProperty("user.id");
env.getProperty("user.userName");

// 装配bean
return new UserInfo(
// 如果LoadBeanFromOuter.properties中对应字段没有,就采用默认值(第二个参数)
env.getProperty("user.id", "201614100130"),
env.getProperty("user.userName", "lauy")
);
}
}

2、通过 @Value 注入外部的值

这个需要在激活好的 yml 或者 properties 配置文件中配置好

1
2
3
4
# yml 文件配置
person:
id: 259753456
gender: A

然后通过 @Value 获取值

1
2
3
4
@Value("${person.id}")
private long id;
@Value("${person.gender}")
private char gender;

3、通过 @ConfigurationProperties 注入外部的值

jdbc.properties配置文件

1
2
3
4
spring.datasource.druid.write.url=jdbc:mysql://localhost:3306/easypoi?serverTimezone=GMT%2b8
spring.datasource.druid.write.username=root
spring.datasource.druid.write.password=write
spring.datasource.druid.write.driver-class-name=com.mysql.cj.jdbc.Driver

Java配置文件

1
2
3
4
5
6
7
8
9
@ConfigurationProperties(prefix = "spring.datasource.druid.read") // 前缀
@PropertySource("classpath:jbdc.properties") // 导入此配置文件中的符合前缀的值
@Data // 注入setter,get方法
public class DruidConfig {
private String url;
private String username;
private String password;
private String driverClassName;
}

4、使用 Spring 表达式语言进行装配

Spring 3 引入了 Spring 表达式语言(Spring Expression Language,SpEL),它能够以一种强大和简洁的方式将值装配到 bean 属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值。使用 SpEL,你可以实现超乎想象的装配效果,这是使用其他的装配技术难以做到的(甚至是不可能的)。

需要了解的第一件事情就是 SpEL 表达式要放到 #{ ... } 之中,这与属性占位符有些类似,属性占位符需要放到 ${ ... } 之中。下面所展现的可能是最简单的 SpEL 表达式了:

1
#{1}

除去 #{ ... } 标记之后,剩下的就是 SpEL 表达式体了,也就是一个数字常量。这个表达式的计算结果就是数字 1,这恐怕并不会让你感到丝毫惊讶。

1
#{T(System).currentTimeMillis()}

它的最终结果是计算表达式的那一刻当前时间的毫秒数。T() 表达式会将 java.lang.System 视为 Java 中对应的类型,因此可以调用其 static 修饰的 currentTimeMillis() 方法。

1
#{ueerInfo.id}

SpEL 表达式可见也可以引用其他的 bean 或其他 bean 的属性。

1
#{systemProperties['user.id']}

可见我们还可以通过 systemProperties 对象引用系统属性。

面向切面的Spring

定义 AOP 术语

描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。图 4.2 展示了这些概念是如何关联在一起的。

通知(Advice)

在 AOP 术语中,切面的工作被称为通知。

通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?

  • 前置通知(Before):在目标方法被调用之前调用通知功能;

  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;

  • 返回通知(After-returning):在目标方法成功执行之后调用通 知;

  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;

  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

连接点(Join point)

连接点是程序执行过程中能够应用通知的所有点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

切点(Poincut)

如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些 AOP 框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。

切面(Aspect)

当抄表员开始一天的工作时,他知道自己要做的事情(报告用电量)和从哪些房屋收集信息。因此,他知道要完成工作所需要的一切东西。切面是通知和切点的结合。通知和切点共同定义了切面的全部内容 —— 它是什么,在何时和何处完成其功能。

引入(Introduction)

引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一个 Auditable 通知类,该类记录了对象最后一次修改时的状态。这很简单,只需一个方法,setLastModified(Date),和一个实例变量来保存这个状态。

织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5 的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的。

Spring在运行时通知对象

通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。如图 4.3 所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标 bean。当代理拦截到方法调用时, 在调用目标 bean 方法之前,会执行切面逻辑。

直到应用需要被代理的 bean 时,Spring 才创建代理对象(懒加载)。如果使用的是 ApplicationContext 的话,在 ApplicationContext 从 BeanFactory 中加载所有 bean 的时候,Spring 才会创建被代理的对象。因为 Spring 运行时才创建代理对象,所以我们不需要特殊的编译器来织入 Spring AOP 的切面。

调用者调用目标对象方法时,包裹目标对象bean的代理类会将方法拦截,调用目标对象方法前会执行切面逻辑,再把调用转发给真正的目标 bean。

通过切点来选择连接点

关于 Spring AOP 的 AspectJ 切点,最重要的一点就是 Spring 仅支持 AspectJ 切点指示器(pointcut designator)的一个子集。表列出了 Spring AOP 所支持的 AspectJ 切点指示器。

AspectJ 指示器 描 述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的bean引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类 型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方 法定义在由指定的注解所标注的类里)
@annotation 限定匹配带有指定注解的连接点

我们使用 execution() 指示器选择 Performance 的 perform() 方法。方法表达式以 “*” 号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的 perform() 方法,无论该方法的入参是什么。

在切点中选择 bean,在这里,我们希望在执行 Performance 的 perform() 方法时应用通知,但限定 bean 的 ID 为 woodstock。

1
execution(* concert.Performance.perform()) and bean('woodstock')

使用注解创建切面

如果一场演出没有观众的话,那不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。

AspectJ 提供了五个注解来定义通知

注解 通知
@After 通知方法会在目标方法返回或抛出异常后调用
@AfterReturning 通知方法会在目标方法返回后调用
@AfterThrowing 通知方法会在目标方法抛出异常后调用
@Around 通知方法会将目标方法封装起来
@Before 通知方法会在目标方法调用之前执行

1、启用自动代理功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableAspectJAutoProxy // 启用自动代理功能: 尽管使用的是@AspectJ注解,但本质上还是Spring 基于代理的切面
@Component
public class PerformanceConfig {
@Bean
public Audience audience() {
return new Audience();
}

@Bean
public TrackCounter trackCounter() {
return new TrackCounter();
}
}

2、定义切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Aspect
public class Audience {

/*@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。*/
@Pointcut("execution(* cn.lauy.service.Performance.perform(..))")
public void performce() { }

/*在演出之前,观众要就坐 takeSeats() 并将手机调至静音状态 silenceCellPhones()。*/
// 同"execution(* cn.lauy.service.Performance.perform(..))"
@Before("performce()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}

@Before("performce()")
public void takeSeats() {
System.out.println("Taking seats");
}

/*如果演出很精彩的话,观众应该会鼓掌喝彩 applause()*/
@AfterReturning("performce()")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}

/*如果演出没有达到观众预期的话,观众会要求退款 demandRefund()。*/
@AfterThrowing("performce()")
public void demandRefund() {
System.out.println("Demanding a refund");
}

}

调用输出:

1
2
3
4
Silencing cell phones
Taking seats
我调用了方法 perform
CLAP CLAP CLAP!!!

3、创建环绕通知

环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。

下面我们用它来完成上述 1 的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Aspect
public class Audience {

/*@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。*/
@Pointcut("execution(* cn.lauy.service.Performance.perform(..))")
public void performce() { }

@Around("performce()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Around Silencing cell phones");
jp.proceed(); // 别忘记调用 proceed() 方法,如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。
System.out.println("Around Taking seats");
jp.proceed(); // 多次调用,可以多次走方法
System.out.println("Around CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println("Around Demanding a refund");
}
}
}

调用输出:

1
2
3
4
5
Around Silencing cell phones
我调用了方法 perform
Around Taking seats
我调用了方法 perform
Around CLAP CLAP CLAP!!!

而且环绕通知执行优先与前置通知,结束后于后置通知,见打印

1
2
3
4
5
6
7
8
9
10
11
Around Silencing cell phones
Silencing cell phones // 第一次调用 jp.proceed();
Taking seats // 执行两次前置通知 @Before
我调用了方法 perform
CLAP CLAP CLAP!!!
Around Taking seats
Silencing cell phones // 第二次调用 jp.proceed();
Taking seats
我调用了方法 perform
CLAP CLAP CLAP!!!
Around CLAP CLAP CLAP!!!

4、处理通知中的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Aspect
public class TrackCounter {

private Map<Integer, Integer> trackCounts = new HashMap<>();

@Pointcut("execution(* cn.lauy.service.Performance.playTrack(int)) && args(trackNumber)")
public void trackPlayed(int trackNumber) { }

@Before("trackPlayed(trackNumber)")
public void countTrack(int trackNumber) {
// 获取key对应的值,或者初始化值0
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount + 1);
}

public int getPlayCount(int trackNumber) {
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}

5、封装成自定义注解

定义注解

1
2
3
4
5
6
7
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Audience {

String name() default "my annotation for Audience";
}

定义切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Aspect
public class Audience {

/*@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。*/
@Pointcut("@annotation(cn.lauy.annotation.Audience)")
public void performce() { }

/*如果演出很精彩的话,观众应该会鼓掌喝彩 applause()*/
@AfterReturning(pointcut = "performce()", returning = "jsonResult")
public void applause(JoinPoint joinPoint, Object jsonResult) {
System.out.println("CLAP CLAP CLAP!!!");
handleLog(joinPoint, null, jsonResult);
}

@Around("performce()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Around Silencing cell phones");
jp.proceed(); // 多次调用,可以多次走方法
System.out.println("Around CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println("Around Demanding a refund");
}
}

protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
System.out.println("jsonResult:" + jsonResult.toString());
}
}

由输出可见

1
2
3
4
5
Around Silencing cell phones
我调用了方法 perform
CLAP CLAP CLAP!!!
jsonResult:Hello world!
Around CLAP CLAP CLAP!!!

study-Aop代码仓库

Springboot使用AOP自定义Log注解

Spring高级技术

将异常映射为 HTTP 状态码

在引入 @ResponseStatus 注解之后,如果控制器方法抛出 SpittleNotFound-Exception 异常的话,响应将会具有 404 状态码,这是因为 Spittle Not Found。

1
2
3
4
5
6
7
8
9
10
11
12
@ResponseStatus(value= HttpStatus.INTERNAL_SERVER_ERROR, reason="My Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {

private static final long serialVersionUID = 2876873023595254542L;

public SpittleNotFoundException() {
}

public SpittleNotFoundException(String message) {
super(message);
}
}

全局异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 或者 @RestControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {

/**
* 自定义验证异常
*/
@ExceptionHandler(BindException.class)
public AjaxResult validatedBindException(BindException e) {
log.error(e.getMessage(), e);
String message = e.getAllErrors().get(0).getDefaultMessage();
return AjaxResult.error(message);
}
}

保护方法的应用

使用注解保护方法

在 Spring Security 中实现方法级安全性的最常见办法是使用特定的注解,将这些注解应用到需要保护的方法上。

Spring Security 提供了三种不同的安全注解:

  • Spring Security 自带的 @Secured 注解;
  • JSR-250 的 @RolesAllowed 注解;
  • 表达式驱动的注解,包括 @PreAuthorize、@PostAuthorize、@PreFilter 和 @PostFilter。

@Secured 和 @RolesAllowed 方案非常类似,能够基于用户所授予的权限限制对方法的访问。当我们需要在方法上定义更灵活的安全规则时,Spring Security 提供了 @PreAuthorize 和 @PostAuthorize,而 @PreFilter/@PostFilter 能够过滤方法返回的以及传入方法的集合。

创建REST API

使用 HTTP 信息转换器

在响应体中返回资源状态

正常情况下,当处理方法返回 Java 对象(除 String 外或 View 的实现以外)时,这个对象会放在模型中并在视图中渲染使用。但是,如果使用了消息转换功能的话,我们需要告诉 Spring 跳过正常的模型/视图流程,并使用消息转换器。有不少方式都能做到这一点,但是最简单的方法是为控制器方法添加 @ResponseBody 注解。

@ResponseBody 能够告诉 Spring 在把数据发送给客户端的时候,要使用某一个消息器,与之类似,@RequestBody 也能告诉 Spring 查找一个消息转换器,将来自客户端的资源表述转换为对象。

1
2
3
4
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
public @ResponseBody Spittle saveSpittle(@RequestBody Spittle spittle) {
return spittleRepository.save(spittle);
}

例如,如果客户端发送的 Spittle 数据是 JSON 表述形式,那么 Content-Type 头部信息可能就会是 “application/json”。在这种情况下,DispatcherServlet 会查找能够将 JSON 转换为 Java 对象的消息转换器。

为控制器默认设置消息转换

Spring 4.0 引入了 @RestController 注解,能够在这个方面给我们提供帮助。如果在控制器类上使用 @RestController 来代替 @Controller 的话,Spring 将会为该控制器的所有处理方法应用消息转换功能。

Springboot

应用程序

启动类

1
2
3
4
5
6
@SpringBootApplication
public class LauyApplication {
public static void main(String[] args) {
SpringApplication.run(LauyApplication.class, args);
}
}

@SpringBootApplication 注释清楚地表明这是一个 Spring 引导应用程序。但是 @SpringBootApplication 中有更多的东西。@SpringBootApplication 是一个组合了其他三个注释的复合应用程序:

  • @SpringBootConfiguration —— 指定这个类为配置类。尽管这个类中还没有太多配置,但是如果需要,可以将 Javabased Spring Framework 配置添加到这个类中。实际上,这个注释是@Configuration 注释的一种特殊形式。
  • @EnableAutoConfiguration —— 启用 Spring 自动配置。稍后我们将详细讨论自动配置。现在,要知道这个注释告诉 Spring Boot 自动配置它认为需要的任何组件。
  • @ComponentScan —— 启用组件扫描。这允许你声明其他带有 @Component@Controller@Service 等注释的类,以便让 Spring 自动发现它们并将它们注册为 Spring 应用程序上下文中的组件。

main() 方法调用 SpringApplication 类上的静态 run() 方法,该方法执行应用程序的实际引导,创建Spring 应用程序上下文。传递给 run() 方法的两个参数是一个配置类和命令行参数。虽然传递给 run() 的配置类不必与引导类相同,但这是最方便、最典型的选择。

处理 web 请求

Spring MVC 的核心是控制器的概念,这是一个处理请求并使用某种信息进行响应的类。

1
2
3
4
5
6
7
8
9
10
11
@Controller// @Component等也行 
@RequestMapping("/user")
public class UserInfoController {

@GetMapping
@ResponseBody
public Result selectOne() {
System.out.println(1);
return Result.success(new HashMap());
}
}

可以看到,这个类是用 @Controller 注释的。@Controller 本身并没有做多少事情。它的主要目的是将该类识别为组件扫描的组件。由于 UserInfoController 是用 @Controller 注释的,因此 Spring 的组件扫描会自动发现它,并在 Spring 应用程序上下文中创建一个 UserInfoController实例作为 bean。

实际上,其他一些注释(包括 @Component@Service@Repository)的用途与 @Controller 类似。你可以用任何其他的注解来有效地注释 UserInfoController,它仍然可以工作。但是,选择 @Controller 更能描述该组件在应用程序中的角色。

推荐-Spring实战(第四版)网页版

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  1. © 2020-2021 Lauy    湘ICP备20003709号

请我喝杯咖啡吧~

支付宝
微信