Spring

参考

B站颜群老师Spring教程,B站雷丰阳老师Spring教程,《Spring实战》
https://zhuanlan.zhihu.com/p/137507309
https://liayun.blog.csdn.net/article/details/115053350

spring简介

Spring起源于2003年,有两大核心(IOC,AOP),如今已经发展到springdata
springboot springframework springsocial,适合做各种项目

Spring注解驱动

可以分为三个部分学习:容器、扩展原理以及Web

容器

第一部分

  1. AnnotationConfigApplicationContext
  2. 组件添加
  3. 组件赋值
  4. 组件注入
  5. AOP
  6. 声明式事务

扩展原理

第二部分

  1. BeanFactoryPostProcessor
  2. BeanDefinitionRegistryPostProcessor
  3. ApplicationListener
  4. Spring容器创建过程

Web

第三部分,其实就是SpringMVC

  1. servlet3.0
  2. 异步请求

容器

一开始学习Spring是通过XML配置文件来定义我们的bean的,这种编写XML
配置文件的方式不仅繁琐,而且还很容易出错。使用Spring注解驱动来开发
就不需要编写XML配置

IOC和DI

在Spring容器的底层,最重要的功能就是IOC和DI,也就是控制反转和依赖
注入
DI和IOC它俩之间的关系是DI不能单独存在,DI需要在IOC的基础上来完成。
在Spring内部,所有的组件都会放到IOC容器中,组件之间的关系通过IOC
容器来自动装配,也就是我们所说的依赖注入

XML配置

在介绍使用注解完成容器中组件的注册、管理及依赖、注入等功能之前,我们
先来看看使用XML配置文件是如何注入bean的。
首先创建一个com.atguigu.bean.Person类

1
2
3
4
5
6
7
8
9
public class Person {
private String name;
private Integer age;
...
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}

在工程的src/main/resources目录下创建Spring的配置文件,例如beans.xml
,通过该配置文件将Person类注入到Spring的IOC容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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-4.2.xsd">

<!-- 注册组件 -->
<bean id="person" class="com.atguigu.bean.Person">
<property name="age" value="18"></property>
<property name="name" value="liayun"></property>
</bean>
</beans>

新建一个MainTest类测试

1
2
3
4
5
6
7
8
public class MainTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new
ClassPathXmlApplicationContext("beans.xml");
Person person = (Person) applicationContext.getBean("person");
System.out.println(person);
}
}

注解配置

在项目的com.meimeixia.config包下创建一个MainConfig类,并在该类上
添加@Configuration注解来标注该类是一个Spring的配置类,也就是告诉
Spring它是一个配置类,最后通过@Bean注解将Person类注入到Spring的
IOC容器中

1
2
3
4
5
6
7
8
9
10
11
// 以前配置文件的方式被替换成了配置类,即配置类==配置文件
// 这个配置类也是一个组件
@Configuration // 告诉Spring这是一个配置类
public class MainConfig {
//@Bean注解是给IOC容器中注册一个bean,类型就是返回值的类型,id默认是用方法名作为id
//@Bean("person") 也可以指定id
@Bean
public Person person() {
return new Person("liayun", 20);
}
}

接下来通过注解式获取bean

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new
AnnotationConfigApplicationContext(MainConfig.class);
Person person = applicationContext.getBean(Person.class);
System.out.println(person);
// Person这个类型的组件在IOC容器中的名字
String[] namesForType = applicationContext.getBeanNamesForType(
Person.class);
for (String name : namesForType) {
System.out.println(name);
}
}

自动扫描组件

在实际项目中,我们更多的是使用Spring的包扫描功能对项目中的包进行扫
描,凡是在指定的包或其子包中的类上标注了@Repository 、 @Service、
@Controller 、@Component 注解的类都会被扫描到,并将这个类注入到
Spring容器中

XML配置包扫描

我们可以在Spring的XML配置文件中配置包的扫描,在配置包扫描时,需要
在Spring的XML配置文件中的beans节点中引入context标签

1
2
3
<!-- 包扫描:只要是标注了我们熟悉的@Controller、@Service、@Repository
、@Component这四个注解中的任何一个的组件,它就会被自动扫描,并加进容器中 -->
<context:component-scan base-package="com.atguigu"></context:component-scan>

这样配置以后,只要在com.atguigu包下或者其的子包下标注了@Repository
、@Service、@Controller、@Component注解的类都会被扫描到,并自动注入
到Spring容器中

注解配置扫描包

只须在我们的MainConfig类上添加@ComponentScan注解,并将扫描的包指定
为com.atguigu即可

1
2
3
4
5
6
7
8
9
10
// 默认会扫描该类所在的包下所有的类
@ComponentScan(value="com.atguigu") // value指定要扫描的包
@Configuration // 告诉Spring这是一个配置类
public class MainConfig {
//@Bean注解是给IOC容器中注册一个bean,类型就是返回值的类型,id默认是用方法名作为id
@Bean("person")
public Person person01() {
return new Person("liayun", 20);
}
}

在src/test/java目录下新建一个单元测试类来进行测试,使用junit需要添
加相应依赖

1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

编写测试类获取IOC容量中的所有bean

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IOCTest {
@SuppressWarnings("resource")
@Test
public void test() {
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("beans.xml");
// 我们现在就来看一下IOC容器中有哪些bean,即容器中所有bean定义的名字
String[] definitionNames = applicationContext.getBeanDefinitionNames();
for (String name : definitionNames) {
System.out.println(name);
}
}
}

@ComponentScan注解

可以设置指定要扫描或排除的组件

1
2
3
4
5
6
public @interface ComponentScan {
//指定Spring扫描的时候按照什么规则只需要包含哪些组件
ComponentScan.Filter[] includeFilters() default {};
//指定Spring扫描的时候按照什么规则排除哪些组件
ComponentScan.Filter[] excludeFilters() default {};
}

两个方法的返回值都是Filter[]数组,在ComponentScan注解类的内部
存在Filter注解类

1
2
3
4
5
6
7
8
public @interface Filter {
FilterType type() default FilterType.ANNOTATION;
@AliasFor("classes")
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] classes() default {};
String[] pattern() default {};
}

扫描时排除注解标注的类

现在有这样一个需求,除了@Controller和@Service标注的组件之外,IOC
容器中剩下的组件我都要,即相当于是我要排除@Controller和@Service
这俩注解标注的组件。要想达到这样一个目的,我们可以在MainConfig类
上通过@ComponentScan注解的excludeFilters()方法实现

1
2
3
4
5
6
7
8
@ComponentScan(value="com.atguigu", excludeFilters={
/*type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,
还是按照正则表达式进行排除等等*/
/*classes:除了@Controller和@Service标注的组件之外,IOC容器中剩下的组件
我都要,即相当于是我要排除@Controller和@Service这俩注解标注的组件*/
@Filter(type=FilterType.ANNOTATION, classes={Controller.class,
Service.class})
})

扫描时只包含注解标注的类

可以使用ComponentScan注解类中的includeFilters()方法来指定Spring
在进行包扫描时,只包含哪些注解标注的类。这里需要注意的是,当我们使
用includeFilters() 方法来指定只包含哪些注解标注的类时,需要禁用
掉默认的过滤规则。
现在有这样一个需求,我们需要Spring在扫描时,只包含@Controller注解
标注的类。要想达到这样一个目的,我们该怎么做呢?可以在MainConfig类
上添加@ComponentScan注解,设置只包含@Controller注解标注的类,并
禁用掉默认的过滤规则

1
2
3
4
5
6
7
@ComponentScan(value="com.atguigu", includeFilters={

/*type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类
型进行排除,还是按照正则表达式进行排除,等等*/
//classes:我们需要Spring在扫描时,只包含@Controller注解标注的类
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false)

注意在使用includeFilters()方法来指定只包含哪些注解标注的类时,结果
信息中会一同输出Spring内部的组件名称,也就是没有用这些注解的Bean

重复注解

@ComponentScan注解上有一个@Repeatable(ComponentScans.class)注解
,在ComponentScans注解类的内部只声明了一个返回ComponentScan[]数组
的value()方法

1
2
3
public @interface ComponentScans {
ComponentScan[] value();
}

可以在一个类上重复使用这个注解

1
2
3
4
5
6
@ComponentScan(value="com.atguigu", includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false) // value指定要扫描的包
@ComponentScan(value="com.atguigu", includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Service.class})
}, useDefaultFilters=false) // value指定要扫描的包

自定义TypeFilter

在使用@ComponentScan注解实现包扫描时,我们可以使用@Filter指定过
滤规则,在@Filter中,通过type来指定过滤的类型。而@Filter 注解中
的type属性是一个FilterType枚举

1
2
3
4
5
6
7
8
9
public enum FilterType {
ANNOTATION,
ASSIGNABLE_TYPE,
ASPECTJ,
REGEX,
CUSTOM;
private FilterType() {
}
}

ANNOTATION

按照注解进行包含或者排除,例如使用@ComponentScan注解进行包扫描
时,如果要想按照注解只包含标注了@Controller注解的组件,那么就
需要像下面这样写了

1
2
3
@ComponentScan(value="com.atguigu", includeFilters={
@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false) // value指定要扫描的包

ASSIGNABLE_TYPE

按照给定的类型进行包含或者排除,例如使用@ComponentScan注解进行
包扫描时,如果要想按照给定的类型只包含BookService类(接口)或
其子类(实现类或子接口)的组件,那么就需要像下面这样写了

1
2
3
4
5
@ComponentScan(value="com.atguigu", includeFilters={
/* 只要是BookService这种类型的组件都会被加载到容器中,不管是
它的子类还是什么它的实现类。只要是BookService这种类型的*/
@Filter(type=FilterType.ASSIGNABLE_TYPE, classes={BookService.class})
}, useDefaultFilters=false) // value指定要扫描的包

ASPECTJ

按照ASPECTJ表达式进行包含或者排除,例如使用@ComponentScan注解
进行包扫描时,按照正则表达式进行过滤,就得像下面这样子写

1
2
3
@ComponentScan(value="com.atguigua", includeFilters={
@Filter(type=FilterType.ASPECTJ, classes={AspectJTypeFilter.class})
}, useDefaultFilters=false) // value指定要扫描的包

REGEX

按照正则表达式进行包含或者排除,例如使用@ComponentScan注解进行
包扫描时,按照正则表达式进行过滤,就得像下面这样子写

1
2
3
@ComponentScan(value="com.atguigua", includeFilters={
@Filter(type=FilterType.REGEX, classes={RegexPatternTypeFilter.class})
}, useDefaultFilters=false) // value指定要扫描的包

CUSTOM

按照自定义规则进行包含或者排除,如果实现自定义规则进行过滤时,自
定义规则的类必须是 TypeFilter 接口的实现类,需要实现该接口中的
match()方法,match()方法的返回值为boolean类型。当返回true时,
表示符合规则,会包含在Spring容器中,当返回false时,表示不符合
规则,那就是一个都不匹配,自然就都不会被包含在Spring容器中

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
package com.atguigua.config;
public class MyTypeFilter implements TypeFilter {
/**
* 参数:
* metadataReader:读取到的当前正在扫描的类的信息
* metadataReaderFactory:可以获取到其他任何类的信息的(工厂)
*/
@Override
public boolean match(MetadataReader metadataReader,
MetadataReaderFactory metadataReaderFactory) throws IOException {
// 获取当前类注解的信息
AnnotationMetadata annotationMetadata =
metadataReader.getAnnotationMetadata();
//获取当前正在扫描的类的类信息,比如说它的类型是什么啊,它实现了什么接口之类的
ClassMetadata classMetadata = metadataReader.getClassMetadata();
// 获取当前类的资源信息,比如说类的路径等信息
Resource resource = metadataReader.getResource();
// 获取当前正在扫描的类的类名
String className = classMetadata.getClassName();
System.out.println("--->" + className);
// 现在来指定一个规则
if (className.contains("er")) {
return true; // 匹配成功,就会被包含在容器中
}
return false; // 这儿我们先让其返回false
}
}

自定义过滤规则时扫描的是com.atguigua包,该包下的每一个类都会进
到这个自定义规则里面进行匹配,包括MyTypeFilter类,配置过程如下

1
2
3
@ComponentScan(value="com.atguigua", includeFilters={
@Filter(type=FilterType.CUSTOM, classes={MyTypeFilter.class})
}, useDefaultFilters=false) // value指定要扫描的包

组件作用域

Spring容器中的组件默认是单例的,在Spring启动时就会实例化并初始
化这些对象,并将其放到Spring容器中,之后,每次获取对象时,直接
从Spring容器中获取,而不再创建对象。如果每次从Spring容器中获取
对象时,都要创建一个新的实例对象,那么该如何处理呢?此时就需要
使用@Scope注解来设置组件的作用域了,内容如下

  • @Scope注解概述
  • 单实例bean作用域
  • 多实例bean作用域
  • 单实例bean作用域如何创建对象?
  • 多实例bean作用域如何创建对象?
  • 单实例bean注意的事项
  • 多实例bean注意的事项
  • 自定义Scope的实现

@Scope注解概述

在@Scope注解中的取值如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
public @interface Scope {
/* 在@Scope注解中可以设置如下值:
ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
*/
@AliasFor("scopeName")
String value() default "";
@AliasFor("value")
String scopeName() default "";
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}

request和session作用域是需要Web环境来支持的,这两个值基本上使
用不到。当我们使用Web容器来运行Spring应用时,如果需要将组件的
实例对象的作用域设置为request和session,那么我们通常会使用

1
2
request.setAttribute("key", object);
session.setAttribute("key", object);

单实例bean作用域

首先在com.atguigua.config包下创建一个配置类,例如MainConfig2
,然后在该配置类中实例化一个Person对象,并将其放置在Spring容
器中

1
2
3
4
5
6
7
@Configuration
public class MainConfig2 {
@Bean("person")
public Person person() {
return new Person("美美侠", 25);
}
}

创建一个测试方法,Spring容器在启动时就会将实例对象加载到Spring
容器中,之后每次从Spring容器中获取实例对象

1
2
3
4
5
6
7
8
9
10
11
12
@SuppressWarnings("resource")
@Test
public void test02() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);

/*获取到的这个Person对象默认是单实例的,因为在IOC容器中给我们加的
这些组件默认都是单实例的,所以说在这儿我们无论多少次获取,获取到的
都是我们之前new的那个实例对象*/
Person person = (Person) applicationContext.getBean("person");
Person person2 = (Person) applicationContext.getBean("person");
System.out.println(person == person2);
}

多实例bean作用域

修改Spring容器中组件的作用域,我们需要借助于@Scope注解。此时我
们将MainConfig2配置类中Person对象的作用域修改成prototype

1
2
3
4
5
6
7
8
9
@Configuration
public class MainConfig2 {
//通过@Scope注解来指定该bean的作用范围,也可以说成是调整作用域
@Scope("prototype")
@Bean("person")
public Person person() {
return new Person("美美侠", 25);
}
}

单实例bean注意的事项

单实例bean是整个应用所共享的,所以需要考虑到线程安全问题,SpringMVC
中的Controller默认是单例的,有些开发者在Controller中创建了一些变量
,那么这些变量实际上就变成共享的了,Controller又可能会被很多线程同
时访问,这些线程并发去修改Controller中的共享变量,此时很有可能会出
现数据错乱的问题,所以使用的时候需要特别注意

自定义Scope

自定义Scope主要分为三个步骤

  1. 实现Scope接口
    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
    public interface Scope {
    /**
    * 返回当前作用域中name对应的bean对象
    * @param name 需要检索的bean对象的名称
    * @param objectFactory 如果name对应的bean对象在当前作用域中没有找
    到,那么可以调用这个objectFactory来创建这个对象
    */
    Object get(String name, ObjectFactory<?> objectFactory);

    /**
    * 将name对应的bean对象从当前作用域中移除
    */
    Object remove(String name);
    /**
    * 用于注册销毁回调,若想要销毁相应的对象,则由Spring容器注册相应
    的销毁回调,而由自定义作用域选择是不是要销毁相应的对象
    */
    void registerDestructionCallback(String name, Runnable callback);
    /**
    * 用于解析相应的上下文数据,比如request作用域将返回request中的属性
    */
    Object resolveContextualObject(String key);

    /**
    * 作用域的会话标识,比如session作用域的会话标识是sessionId
    */
    String getConversationId();
    }
  2. 将自定义Scope注册到容器中,此时需要调用ConfigurableBeanFactory
    #registerScope这个方法
    1
    void registerScope(String scopeName,Scope scope);
  3. 第三步,使用自定义的作用域。也就是在定义bean的时候,指定bean的
    scope属性为自定义的作用域名称

一个自定义Scope实现

例如我们来实现一个线程级别的bean作用域,同一个线程中同名的bean是同
一个实例,不同的线程中的bean是不同的实例,要求bean在线程中是共享的
,所以我们可以通过ThreadLocal来实现,ThreadLocal可以实现线程中数
据的共享

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

/*自定义本地线程级别的bean作用域,不同的线程中的bean是不同的实例
,同一个线程中同名的bean是同一个实例*/
public class ThreadScope implements Scope {
public static final String THREAD_SCOPE = "thread";
private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {
@Override
protected Object initialValue() {
return new HashMap<>();
}
};
/**
* 返回当前作用域中name对应的bean对象
* @param name 需要检索的bean对象的名称
* @param objectFactory 如果name对应的bean对象在当前作用域中没有找到,
那么可以调用这个objectFactory来创建这个bean对象
*/
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object bean = beanMap.get().get(name);
if (Objects.isNull(bean)) {
bean = objectFactory.getObject();
beanMap.get().put(name, bean);
}
return bean;
}
/**
* 将name对应的bean对象从当前作用域中移除
*/
@Override
public Object remove(String name) {
return this.beanMap.get().remove(name);
}
/**
* 用于注册销毁回调,若想要销毁相应的对象,则由Spring容器注册相
应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象
*/
// bean作用域范围结束的时候调用的方法,用于bean的清理
@Override
public void registerDestructionCallback(String name, Runnable callback) {
System.out.println(name);
}
/**
* 用于解析相应的上下文数据,比如request作用域将返回request中的属性
*/
@Override
public Object resolveContextualObject(String key) {
return null;
}
/**
* 作用域的会话标识,比如session作用域的会话标识是sessionId
*/
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}

在ThreadScope类中,我们定义了一个THREAD_SCOPE常量,该常量是在定
义bean的时候给scope使用的

1
2
3
4
5
6
7
8
9
10

@Configuration
public class MainConfig3 {
@Scope("thread")
@Bean("person")
public Person person() {
System.out.println("给容器中添加咱们这个Person对象...");
return new Person("美美侠", 25);
}
}

懒加载

Spring在启动时,默认会将单实例bean进行实例化,并加载到Spring容
器中去。也就是说,单实例bean默认是在Spring容器启动的时候创建对
象,并且还会将对象加载到Spring容器中。如果我们需要对某个bean进
行延迟加载,那么该如何处理呢?此时,就需要使用到@Lazy注解了。
懒加载就是Spring容器启动的时候,先不创建对象,在第一次使用(获取
)bean的时候再来创建对象,并进行一些初始化

1
2
3
4
5
6
7
8
9
@Configuration
public class MainConfig2 {
@Lazy
@Bean("person")
public Person person() {
System.out.println("给容器中添加咱们这个Person对象...");
return new Person("美美侠", 25);
}
}

使用@Lazy注解标注后,单实例bean对象只是在第一次从Spring容器中获
取时被创建,以后每次获取bean 对象时,直接返回创建好的对象,仅针
对单实例bean生效

按照条件注册bean

Spring支持按照条件向IOC容器中注册bean,满足条件的bean就会被注册到
IOC容器中,不满足条件的bean就不会被注册到IOC容器中。@Conditional
注解可以按照一定的条件进行判断,满足条件向容器中注册bean,不满足
条件就不向容器中注册bean

1
2
3
4
5
6
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}

从@Conditional注解的源码来看,@Conditional注解不仅可以添加到类上
,也可以添加到方法上。在@Conditional注解中,还存在一个Condition
类型或者其子类型的Class对象数组

1
2
3
4
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

使用@Conditional注解时,需要写一个类来实现Spring提供的Condition
接口,它会匹配@Conditional所符合的方法

不带条件注册bean

MainConfig2配置类中新增person01()方法和person02()方法,并为这两
个方法添加@Bean注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class MainConfig2 {
@Lazy
@Bean("person")
public Person person() {
System.out.println("给容器中添加咱们这个Person对象...");
return new Person("美美侠", 25);
}
@Bean("bill")
public Person person01() {
return new Person("Bill Gates", 62);
}
@Bean("linus")
public Person person02() {
return new Person("linus", 48);
}
}

测试是否被注册到IOC容器,三个bean都被注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test06() {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(MainConfig2.class);
//我们现在就来看一下IOC容器中Person这种类型的bean都有哪些
String[] namesForType = applicationContext.getBeanNamesForType(
Person.class);
for (String name : namesForType) {
System.out.println(name);
}
Map<String, Person> persons = applicationContext.getBeansOfType(
Person.class); // 找到这个Person类型的所有bean
System.out.println(persons);
}

带条件注册bean

我们现在提出一个新的需求,比如如果当前操作系统是Windows操作系统
,那么就向Spring 容器中注册名称为bill 的Person 对象,如果当前
操作系统是Linux 操作系统,那么就向Spring容器中注册名称为linus
的Person对象。要想实现这个需求就得要使用@Conditional注解了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test06() {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(MainConfig2.class);
// 我们现在就来看一下IOC容器中Person这种类型的bean都有哪些
String[] namesForType = applicationContext.getBeanNamesForType(
Person.class);
// 拿到IOC运行环境
ConfigurableEnvironment environment = applicationContext.getEnvironment();
// 动态获取坏境变量的值,例如操作系统的名字
String property = environment.getProperty("os.name");
// 获取操作系统的名字,例如Windows 10
System.out.println(property);
for (String name : namesForType) {
System.out.println(name);
}
Map<String, Person> persons = applicationContext.getBeansOfType(
Person.class); // 找到这个Person类型的所有bean
System.out.println(persons);
}

要想使用@Conditional注解,我们需要实现Condition接口来为@Conditional
注解设置条件,所以,这里我们创建了两个实现Condition接口的类,它们分别
是LinuxCondition和WindowsCondition

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
public class LinuxCondition implements Condition {

/**
* ConditionContext:判断条件能使用的上下文(环境)
* AnnotatedTypeMetadata:当前标注了@Conditional注解的注释信息
*/
@Override
public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 判断操作系统是否是Linux系统
/* 1. 获取到bean的创建工厂(能获取到IOC容器使用到的BeanFactory,
它就是创建对象以及进行装配的工厂)*/
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 2. 获取到类加载器
ClassLoader classLoader = context.getClassLoader();
/* 3. 获取当前环境信息,它里面就封装了我们这个当前运行时的一些信
息,包括环境变量,以及包括虚拟机的一些变量*/
Environment environment = context.getEnvironment();
// 4. 获取到bean定义的注册类
BeanDefinitionRegistry registry = context.getRegistry();
/*在这儿还可以做更多的判断,比如说我判断一下Spring容器中是不是包含
有某一个bean,就像下面这样,如果Spring容器中果真包含有名称为person
的bean,那么就做些什么事情...*/
boolean definition = registry.containsBeanDefinition("person");
String property = environment.getProperty("os.name");
if (property.contains("linux")) {
return true;
}
return false;
}
}

通过context 的getRegistry() 方法获取到的bean定义的注册对象,
实际是一个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Spring容器中所有的bean都可以通过BeanDefinitionRegistry对象来进行注册
//因此我们可以通过它来查看Spring容器中到底注册了哪些bean
public interface BeanDefinitionRegistry extends AliasRegistry {
//向Spring容器中注册一个bean
void registerBeanDefinition(String var1, BeanDefinition var2)
throws BeanDefinitionStoreException;
//移除一个bean
void removeBeanDefinition(String var1) throws NoSuchBeanDefinitionException;
//查询某一个bean的定义信息
BeanDefinition getBeanDefinition(String var1)
throws NoSuchBeanDefinitionException;
//判断Spring容器中是否包含有某一个bean的定义
boolean containsBeanDefinition(String var1);
String[] getBeanDefinitionNames();
int getBeanDefinitionCount();
boolean isBeanNameInUse(String var1);
}
1
2
3
4
5
6
7
8
9
10
11
12
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
String property = environment.getProperty("os.name");
if (property.contains("Windows")) {
return true;
}
return false;
}
}

然后就需要在MainConfig2配置类中使用@Conditional注解添加条件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class MainConfig2 {
@Lazy
@Bean("person")
public Person person() {
System.out.println("给容器中添加咱们这个Person对象...");
return new Person("美美侠", 25);
}
@Conditional({WindowsCondition.class})
@Bean("bill")
public Person person01() {
return new Person("Bill Gates", 62);
}
@Conditional({LinuxCondition.class})
@Bean("linus")
public Person person02() {
return new Person("linus", 48);
}
}

此外@Conditional注解也可以标注在类上,标注在类上的含义是:只
有满足了当前条件,这个配置类中配置的所有bean注册才能生效,也
就是对配置类中的组件进行统一设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 对配置类中的组件进行统一设置
// 满足当前条件,这个类中配置的所有bean注册才能生效
@Conditional({WindowsCondition.class})
@Configuration
public class MainConfig2 {
@Lazy
@Bean("person")
public Person person() {
System.out.println("给容器中添加咱们这个Person对象...");
return new Person("美美侠", 25);
}
@Bean("bill")
public Person person01() {
return new Person("Bill Gates", 62);
}
@Conditional({LinuxCondition.class})
@Bean("linus")
public Person person02() {
return new Person("linus", 48);
}
}

此时我们在运行IOCTest类中的test06()方法时,在Run Configurations
配置环境中VM options栏中设置一个-Dos.name=linux参数,将操作系统
模拟为了linux系统

@Conditional的扩展注解

@Conditional与@Profile的对比

Spring 3.0也有一些和@Conditional相似的注解,它们是Spring SPEL表达
式和Spring Profiles注解,但是Spring 4.0之后的@Conditional 注解要
比@Profile注解更加高级。@Profile注解用来加载应用程序的环境,该注解
仅限于根据预定义属性编写条件检查,而@Conditional注解则没有此限制

  1. Spring 3.0中的@Profile仅用于编写基于Environment变量的条件检查
    。配置文件可用于基于环境加载应用程序配置
  2. Spring 4.0之后的@Conditional注解允许开发人员为条件检查定义用户
    定义的策略。此外@Conditional注解还可以用于条件bean注册

环境搭建

下载jar包 spring-framework-4.3.9.RELEASE,基础项目需要使用如下jar包

  • spring-aop.jar 开发AOP特性
  • spring-beans.jar 处理Bean
  • spring-context.jar 处理上下文
  • spring-core.jar 核心jar
  • spring-expression.jar spring表达式 EL/jtsl
  • coomons-logging.jar 增加第三方日志jar

编写配置文件

配置文件applicationContext.xml,SprintBoot中对应的是application.yml

IOC控制反转(依赖注入DI)

在springIOC容器中创建对象并且给属性赋值,之后就可以直接拿取容器中的对象
,要求对象必需有无参构造器。对象的整个创建和销毁过程都是由Spring控制,我
门只需要从容器中获取对象即可,这就是控制反转

1
2
3
4
<!--  首先在配置文件中注册对象 -->
<bean id="student" lazy-init="true" class=包名+类名>
...
</bean>
1
2
3
4
5
public class Student{
private int age;
private String name;
...
}

可以直接从容器中获取对象,两种方式的区别

  1. 省略new
  2. 省略对象属性的赋值
  3. spring容器自动创建新对象并且自动赋值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args){
    //以往创建对象
    Student stu=new Student();
    //spring上下文对象
    ApplicationContext context=new ClassPathXmlApplicationContext(
    "applicationContext.xml");
    //根据id获取对象
    Student stu=(Student)context.getBean("student");
    }

Spring对象的创建时间

配置文件中可以配置一个个bean,默认在启动Spring的时候就会创建一个个
对象,并执行对象的初始化。bean中有一个属性lazy-init,默认情况时
default/false就表明在启动Spring的时候创建对象,如果是true的话
只会在getBean的时候创建对象,这样可以减少内存消耗

1
2
3
4
5
6
7
8
<!--  springioc容器 -->
<bean id="student" lazy-init="true" scope="singleton/prototype"
class=包名+类名>
<!-- 类的属性 -->
<property name="age" value="20"></property>
<property name="name" value="fsd"></property>
...
</bean>

bean中还有一个属性是scope,默认是singleton,表示这个类是单例类,也就是
说不管从容器中拿出多少个student,实际上拿出的是同一个student。如果指定
为prototype则说明会创建多例对象

set注入

简单类型用value赋值,如果是对象类型用ref=”需要引用的id”,course依赖teacher
teacher从其他地方注入course中,赋值默认使用set方法,底层通过反射实现

1
2
3
4
5
6
7
8
9
public class Teacher{
private int age;
private String name;
}
public class Course{
private int number;
private String name;
private Teachre teacher;
}
1
2
3
4
5
6
7
<bean id=teacher class="....Teacher"> 
...
</bean>
<bean id=course class="....Course">
...
<property name="teacher" ref="teacher"></property>
</bean>

构造器注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Teacher(int age,String name)
{
this.age=age;
this.name=name;
}
public Teacher(){

}
public Course(int number,String name,Teacher teacher){
this.number=number;
this.name=name;
this.teacher=teacher;
}
public Course(){

}
1
2
3
4
5
6
7
8
9
10
<bean id=teacher class="....Teacher"> 
<!-- 如果顺序与构造器不一致可以用index/name/type 指定-->
<constructor-arg value="20" index="0"></constructor>
<constructor-arg value="fsd" index="1"></constructor>
</bean>
<bean id=course class="....Course">
<constructor-arg value="111" name="number"></constructor>
<constructor-arg value="fsfsd"></constructor>
<constructor-arg ref="teacher"></constructor>
</bean>

p命名空间注入

在容器中引入p命名空间,xmlns:p=”http://wwww.springframework.org/scheam/p"

1
2
3
4
5
6
<bean id=teacher class="....Teacher" p:age="20" p:name="vxc"> 
</bean>
<bean id=course class="....Course">
...
<property p:name="fsd" p:number="202" p:teacher-ref="teacher"></property>
</bean>

注入各种数据类型

List/Map/Set

1
2
3
4
5
6
7
public class All{
private List<String> list;
private String[] array;
private HashMap<String,Object> map;
private HashSet<String> set;
...
}
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
<bean id="All" class="..."> 
<property name="list">
<list>
<value>dfs</value>
<value>af</value>
<value>zv</value>
</list>
</property>
<property name="array">
<array>
<value></value>
</array>
</property>
<property name="set">
<set>
<value></value>
</set>
</property>
<property name="map">
<map>
<entry>
<key>
<value>fsda</value>
</key>
<value>fds</value>
</entry>
<entry>
<key>
<value>fsda</value>
</key>
<value>fds</value>
</entry>
</map>
</property>
</bean>

特殊值注入问题以及类型自动匹配

value中表示符号

  • < 变为 &lt;
  • & 变为 &amp;
  • > 变为 &gt;

自动装配(只适用于引用类型)

约定优于配置,不需要在bean中配置,能够自动找到引用,course类中有一个
teacher属性,ioc容器中有一个id为teachar的bean,id值=属性名。可以头
文件全局设置default-autowire=””,自动装配可以减少代码量但是降低可读性

  • byName
  • byType 根据bean类型自动装配,弊端是有两个teacher会报错
  • constructor bean类型是否与构造方法参数类型一致,本质是byType
1
2
3
4
5
6
<bean id="course" class="....Course" autowire="byName"> 
...
<!--
<property name="teacher" ref="teacher"></property>
-->
</bean>

注解

通过注解的形式将bean以及相应的属性值放入ioc容器,用于简化配置

配置扫描器

将要扫描的包放入扫描器,扫描器会扫描该包下的所有类如果发现有注解就会将
其放入ioc容器中

1
2
<context:componetn-scan base-package="org.lanqiao.bao,...">  
</context:componetn-scan>

Required

应用于bean属性的setter方法,表明受影响的bean属性在配置时必须放在
XML配置文件中

1
2
3
4
5
6
7
8
9
10
11
12
public class Student {
private Integer age;
private String name;
@Required
public void setAge(Integer age) {
this.age = age;
}
@Required
public void setName(String name) {
this.name = name;
}
}
1
2
3
4
<bean id="student" class="com.tutorialspoint.Student">
<property name="name" value="Zara" />
<property name="age" value="11">
</bean>

Resource

它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class Student
{

}
public class Teacher
{
/*
如果有name属性,就会按照id进行匹配,如果
没有name属性,默认会按照属性的名称进行匹配,
如果属性名称不匹配则会按照class类型进行匹配
*/
@Resource(name="student")
private Student student;
}

Qualifier

这个注解跟Autowired配合使用

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public interface Student
{

}
public class Teacher
{
//相当于Resource
@Autowired
@Qualifier("name")
private Student student;
}

使用注解声明事务

通过事务使所有方法(例如增删查改)全部成功或者失败

1
2
3
4
5
6
//使add方法变为一个事务,方法中的内容要么都执行要么都不执行
@Transactional(readOnly=false,propagation=Propagation.REQUIRED)
public void add()
{
...
}

AOP面向切面编程

比如有几个方法含有一些相同代码,如果把这些代码抽成一个方法,这就是面
向对象编程,如果这个方法改名字那么所有引用这个方法的地方都要改名字,
如果这个方法因为逻辑要改变位置那么所有引用这个方法的地方都要进行修
改。以上问题可以用AOP解决。根据以上如果许多方法需要使用这个抽取的方
法,不需要写在那些方法中,直接切入那些方法中,如果修改抽取方法的名
字或位置时不需要修改,因为这些方法并没有引用

切入点与切面

切入点定义抽取方法在哪个位置执行,切面就是抽取方法切到切入点

通知

  1. 前置通知
    在切入点add方法执行之前插入的通知
  2. 后置通知
    在切入点add方法执行之后插入的通知
  3. 异常通知
    在切入点add方法抛出异常时插入的通知
  4. 环绕通知
    可以贯穿切入点add方法执行的通知
  5. 最终通知
    当切入点add方法执行完毕插入的通知(不论正常还是异常)

类变为通知

需要通过配置将通知与切入点关联,通知类与切入类都需放入ioc容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<aop:config> 
<aop:pointcut expression=
"
execution(public void org.lanqiao.service.Stu.add(org..Student))
or execution(public void add(org..Student))
or execution(public * add(org..Student))
or execution(public void *(org..Student))
or execution(public void add(..))
or execution(* org.lanqiao.service.Stu.*.*(..))
or execution(* org.lanqiao.service.Stu..*.*(..))
"
id="pointcut"/>
<aop:advisor advice-ref="printm" pointcut-ref="pointcut/>
</aop:config>
  1. 实现接口
  • 前置通知 MethodBeforeAdvice before(Method method,Object[] args,
    Object target)
  • 后置通知 AfterReturningAdvice afterReturning(Object returnValue,
    Method method,Object[] args,Object target) target就是调用切入点的对象,
    method就是切入点,args就是好切入点参数,切入点返回值
  • 异常通知 ThrowsAdvice afterThrowing(Method method,Object[] args,
    Object target,Throwable ex),是public方法
  • 环绕通知 可以包含前置后置异常最终,可以获取目标方法的全部控制权
    (目标方法是否执行、执行之前、执行之后、参数、返回值)
    MethodInterceptor,底层是拦截器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public Object invoke(MethodInvocation invocation) throws Throwable{
    try{
    //前置通知
    System.out.println("用环绕通知实现的前置通知");
    //控制目标方法的执行,如果不写则不会执行目标方法add,但是通知会执行
    Object result=invocation.proceed(); //接收返回值
    //后置通知
    System.out.println("用环绕通知实现的后置通知");
    System.out.println("target="+invocation.getThis()+
    "method="+invocation.getMethod()+"args="+
    invocation.getArguments());
    }catch(Exception e){
    //异常通知
    System.out.println("用环绕通知实现的异常通知");
    }
    return result;
    }

基于注解形式的AOP

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
37
38
39
40
41
42
@Component("logann") //加入ioc容器
@Aspect //变为通知
public class LogAnn{

@Before
("execution(public void org.lanqiao.service.Stu.add(org..Student))")
public void myBefore(JoinPoint jp){ //获取目标对象
System.out.println("注解形式的前置通知");
System.out.println("target"+jp.getTarget()+"method"+
jp.getSignature()+"args"+jp.getArgs());
}
//如果有返回值需要声明返回值参数名
@AfterReturning
(pointcut=
"execution(public void org.lanqiao.service.Stu.add(org..Student))"
,returning="returningValue")
public void myAfter(JoinPoint jp,Object returningValue){
Sysetm.out.println("注解形式的后置通知")
}
@Around
("execution(public void org.lanqiao.service.Stu.add(org..Student))")
public void myAround(ProceedingJoinPoint jp){
try{
jp.proceed();
}catch(Throwable ex){

}finally{
//最终通知
}
}
@AfterThrowing
("execution(public void org.lanqiao.service.Stu.add(org..Student))")
public vid myException(JoinPoint jp,NullPointerException ex){
System.out.println("捕获特定异常");
}
@After
("execution(public void org.lanqiao.service.Stu.add(org..Student))")
public void myAfter(){

}

}

spring开发web项目

springioc容器初始化,在Java程序中new ClassPath…

  • 将IOC容器中的所有bean实例化为对象
  • 将各个bean依赖的属性值注入进去

web程序初始化IOC容器

web程序中没有统一的入口,如果很多类需要使用容器那么每个类中都要实例
化容器,耗费性能,启动web项目时可以通过监听器将IOC容器实例化

监听器

spring-web.jar中有写好的监听器,在web.xml中加载监听器,服务器启动时
会执行监听器默认springioc配置文件名为applicationContext.xml

1
2
3
4
<listener> 
<listener-class>org.springframework.web.context.ContextLoaderListener.class
</listener-class>
</listener>

拆分配置文件

Java项目

  • applciationContext1.xml
  • applciationContext2.xml
  • applciationContext3.xml

web项目

合并多个配置文件,在web.xml中指定,可以拿取IOC容器

1
2
3
4
5
6
7
8
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext-Dao.xml,
classpath:applicationContext-*.xml,
...
</param-value>
</context-param>

也可以在主配置文件中引入子配置文件

1
2
<import resource="applicationContext-Dao.xml"/>
<import resource="applicationContext-*.xml"/>

常用注解总结

@SpringBootApplication

这个注解是Spring Boot 项目的基石,创建SpringBoot 项目之后会默
认在主类加上,标注在某个类上就说明该类是主配置类,运行该类main
方法启动应用

1
2
3
4
5
6
7
@SpringBootApplication
public class HelloWorldMainApplication {
public static void main(String[] args) {
//spring应用启动起来
SpringApplication.run(HelloWorldMainApplication.class,args);
}
}

我们可以把该注解看作是 @Configuration、@EnableAutoConfiguration
、@ComponentScan 注解的集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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 {
...
}

根据SpringBoot 官网,这三个注解的作用分别是:

  1. @EnableAutoConfiguration:启用SpringBoot 的自动配置机制
  2. @ComponentScan: 扫描被@Component (@Service,@Controller)注解
    的 bean,注解默认会扫描该类所在的包下所有的类
  3. @Configuration:允许在Spring 上下文中注册额外的bean 或导入其他
    配置类

@Autowired

自动导入对象到类中,被注入进的类同样要被Spring 容器管理比如:
Service 类注入到Controller 类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public interface Student
{

}
public class Teacher
{
/*
如果Student的实现类只有一个,那么相当于
Student student=new Studentimpl();
如果Student实现类有多个,则会按照属性名称
进行匹配
*/
@Autowired
private Student student;
}

Component,@Repository,@Service, @Controller

我们一般使用 @Autowired 注解让Spring 容器帮我们自动装配 bean。要
想把类标识成可用于@Autowired 注解自动装配的bean 的类,可以采用以
下注解实现:

  1. @Component :通用的注解,可标注任意类为 Spring 组件。如果一个
    Bean 不知道属于哪个层,可以使用@Component 注解标注
  2. @Repository : 对应持久层即Dao 层,主要用于数据库相关操作
  3. @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到Dao层
  4. @Controller : 对应Spring MVC 控制层,主要用户接受用户请求并
    调用Service 层返回数据给前端页面
1
2
3
4
5
6
7
8
9
10
@Component //默认id是dao
//也可以自定义id @Component("dd")
public class Dao{
@Autowired //byType类型的自动装配
@Qualifier("teacher") //byName自动装配按照id
private Teacher teacher;
}
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
Dao person = (Dao)context.getBean("dao");

@RestController

@RestController 注解是@Controller 和@ResponseBody 的合集,
表示这是个控制器bean,并且是将函数的返回值直接填入HTTP 响应
体中,是REST 风格的控制器,返回 JSON 或 XML 形式数据

1
2
3
4
5
6
7
8
9
10
@Controller
//能够处理请求
public class HelloController {
@ResponseBody //能够直接将数据返回,也可以写在类上面
@RequestMapping("/hello") //接收来自浏览器的hello请求
public String hello()
{
return "hello";
}
}

@Scope

声明Spring Bean 的作用域,使用方法:

1
2
3
4
5
@Bean
@Scope("singleton")
public Person personsingleton(){
return new Person();
}

四种常见的Spring Bean 的作用域:

  1. singleton : 唯一bean 实例,Spring 中的bean 默认都是单例的
  2. prototype : 每次请求都会创建一个新的bean 实例
  3. request : 每一次HTTP 请求都会产生一个新的bean,该bean 仅
    在当前 HTTP request 内有效
  4. session : 每一次HTTP 请求都会产生一个新的 bean,该 bean
    仅在当前 HTTP session 内有效

@Configuration

一般用来声明配置类,可以使用 @Component 注解替代,不过使用
Configuration 注解声明配置类更加语义化

1
2
3
4
5
6
7
@Configuration
public class AppConfig{
@Bean
public Person personsingleton(){
return new Personimpl();
}
}

Get 请求

@GetMapping(“users”) 等价于 @RequestMapping(value=”/users”,
method=RequestMethod.GET),请求从服务器获取特定资源

1
2
3
4
@GetMapping("/users")
public ResponseEntity<List<User>> getAll(){
return user.findall();
}

POST 请求

@PostMapping(“users”) 等价于 @RequestMapping(value=”/users”,
method=RequestMethod.POST),在服务器上创建一个新的资源

1
2
3
4
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user){
return user.save(user);
}

PUT 请求

@PutMapping(“/users/{userId}”) 等价于 @RequestMapping(
value=”/users/{userId}”,method=RequestMethod.PUT),更
新服务器上的资源(客户端提供更新后的整个资源)。举个例子:
PUT /users/12(更新编号为 12 的学生)

1
2
3
4
5
@PutMapping("/users/{userId}")
public ResponseEntity<User> updateUser(@PathVariable(value="userId")
Long userid,@Valid @RequestBody User user){
...
}

DELETE 请求

@DeleteMapping(“/users/{userId}”) 等价于 @RequestMapping(
value=”/users/{userId}”,method=RequestMethod.DELETE),从
服务器删除特定的资源

1
2
3
4
@DeleteMapping("/users/{userId}")
public ResponseEntity<User> deleteUser(@PathVariable(value="userId")){
...
}

PATCH 请求

更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),
一般实际项目中,我们都是PUT不够用了之后才用PATCH请求去更新数据

1
2
3
4
@PatchMapping("/profile")
public ResponseEntity<User> updateUser(@RequestBody User user){
...
}

@PathVariable 和 @RequestParam

@PathVariable用于获取路径参数,@RequestParam用于获取查询参数

1
2
3
4
5
6
7
8
9
10
11
12
@Controller //使类变为控制器
//SpringMvcHan/welcome/abc?type=web
@RequestMapping("Spring") //映射路径
public class SpringMvcHandler{
@RequestMapping("welcome/{name}")
public String welcome(
@PathVariable("name") String name,
@RequestParam(value="uage",required=false,defaultValue="23")
String type){
..
}
}

@RequestBody

用于读取Request 请求(可能是POST,PUT,DELETE,GET 请求)的body
部分并且Content-Type 为 application/json 格式的数据,接收到
数据之后会自动将数据绑定到 Java 对象上去。
系统会使用HttpMessageConverter 或者自定义的HttpMessageConverter
将请求的body 中的json 字符串转换为java 对象

1
2
3
4
5
6
7
8
9
@PostMapping("/profile")
public ResponseEntity<User> updateUser(@RequestBody User user){
...
}
public class User{
private String name;
private String password;
private String fullName;
}

我们发送post 请求到这个接口,并且body 携带JSON 数据:

1
{"name":"coder","fullName":"shuangkou","password":"123456"}

这样我们的后端就可以直接把json 格式的数据映射到我们的User 类上,
需要注意的是:一个请求方法只可以有一个@RequestBody,但是可以有
多个@RequestParam和@PathVariable。
如果你的方法必须要用两个 @RequestBody来接受数据的话,大概率是你
的数据库设计或者系统设计出问题了

读取配置信息

很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信
认证的相关配置信息等等放到配置文件中。比如 application.yml 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wuhan2020: 武汉加油!中国加油!

my-profile:
name: Guide哥
email: koushuangbwcx@163.com

library:
location: 湖北武汉加油中国加油
books:
- name: 天才基本法
description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天
- name: 时间的秩序
description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?
- name: 了不起的我
description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系?

@Value

使用@Value(“${property}”) 读取比较简单的配置信息

1
2
@Value("${wuhan2020}")
String wuhan;

@ConfigurationProperties

通过@ConfigurationProperties 读取配置信息并与bean 绑定

1
2
3
4
5
6
7
8
9
10
@Component
@ConfigurationProperties(prefix="library")
class libraryproperties{
private String location;
private List<Book> books;
static class Book{
String name;
String description;
}
}

PropertySource

@PropertySource 读取指定properties 文件

1
2
3
4
5
6
@Component
PropertySource("classpath:website.properties")
class libraryproperties{
@Value("${wuhan2020}")
private String name;
}

参数校验

数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,
我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直
接通过一些HTTP 工具直接向后端请求一些违法数据。校验的时候我们
实际用的是Hibernate Validator 框架

一些常用的字段验证的注解

  • @NotEmpty 被注释的字符串的不能为 null 也不能为空
  • @NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符
  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
  • @Email 被注释的元素必须是 Email 格式。
  • @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定
    的最小值
  • @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定
    的最大值
  • @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等
    于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于
    等于指定的最大值
  • @Size(max=, min=)被注释的元素的大小必须在指定的范围内
  • @Digits (integer, fraction)被注释的元素必须是一个数字,其值
    必须在可接受的范围内
  • @Past被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出
MethodArgumentNotValidException

1
2
3
4
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user){
return user.save(user);
}

验证请求参数

一定一定不要忘记在类上加上Validated 注解了,这个参数可以告诉
Spring 去校验方法参数

1
2
3
4
5
6
@PostMapping("/users")
@Validated
public ResponseEntity<User> createUser(@Valid @PathVariable("id")
@Max(5,message=)){
...
}

全局处理Controller 层异常

介绍一下我们Spring 项目必备的全局处理Controller 层异常

  1. @ControllerAdvice :注解定义全局异常处理类
  2. @ExceptionHandler :注解声明异常处理方法

如何使用呢?拿我们在参数校验这块来举例子。如果方法参数不对的话就
会抛出MethodArgumentNotValidException,我们来处理这个异常

@Transactional

在要开启事务的方法上使用@Transactional注解即可!

1
2
3
4
@Transactional(rollbackFor=Exception.class)
public void save(){
...
}

Exception 分为运行时异常 RuntimeException 和非运行时异常。在
@Transactional注解中如果不配置rollbackFor 属性,那么事物只会
在遇到 RuntimeException 的时候才会回滚,加上rollbackFor=
Exception.class,可以让事物在遇到非运行时异常时也回滚。
@Transactional 注解一般用在可以作用在类或者方法上

  1. 作用于类:当把@Transactional 注解放在类上时,表示所有该类的
    public 方法都配置相同的事务属性信息
  2. 作用于方法:类配置@Transactional,方法也配置@Transactional
    ,方法的事务会覆盖类的事务配置信息

测试相关

@ActiveProfiles一般作用于测试类上, 用于声明生效的Spring配置文件

1
2
3
4
5
6
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
......
}
  1. @Test声明一个方法为测试方法
  2. @Transactional被声明的测试方法的数据会回滚,避免污染测试数据
  3. @WithMockUser Spring Security提供的,用来模拟一个真实用户
    ,并且可以赋予权限
1
2
3
4
5
6
@Test
@Transactional
@WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER")
void should_import_student_success() throws Exception {
......
}
Author: 高明
Link: https://skysea-gaoming.github.io/2020/02/28/Spring/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.