Spring学习笔记

Spring Framework 是一个开源的 Java/Java EE 全功能栈(full-stack)的应用程序框架,以 Apache 许可证形式发布,也有 .NET 平台上的移植版本。
关键字:轻量级非侵入式一站式模块化,其目的是用于简化企业级应用程序开发。
可以说 Spring 是一个容器框架,用于配置 bean、处理和维护 bean 之间的关系的框架
注意:Spring 中的 bean 概念可以是 java 中的任何一种对象,比如可以是 javabean、service、action、数据源、dao、ioc(控制反转)、di(依赖注入)等
可以说 Spring 贯穿各个层,上至 web 下至持久层,很厉害…

快速入门

一般来说,当 Spring 加载的时候,会自动创建配置的 bean 对象(当作用域是 singleton 的时候),并加载到内存中去;使用 property 标签来注入对象(属性)
最简单的栗子,使用 Spring 获取一个对象,不需要再 new 了,首先定义一个简单的 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
public class User {
private String name;
private int age;

@Override
public String toString() {
return "Name -->" + name + "Age -->" + age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

然后是在 xml 进行配置,文件名一般写 applicationContext 放在 src 目录下,如果 bean 对象引用了其他对象,可以使用 property 的 ref 属性设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="user" class="com.bfchengnuo.domain.User">
<property name="name" value="佳芷"/>
<property name="age" value="12"/>
</bean>

<bean id="loli" class="com.bfchengnuo.domain.Loli">
<property name="desc" value="lovely"/>
<property name="user" ref="user"/>
</bean>
</beans>

进行测试下,在启动时 Spring 就给你创建好对象了,并且是单例的,默认情况下;最好是使用接口,这样以后就不需要改动代码了,只改配置文件即可

1
2
3
4
5
// 解析配置文件并且创建其中的 bean 到内存中,非常耗费资源,一般写成工具类 单例模式
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
Loli loli = (Loli) ac.getBean("loli");

System.out.println(loli);

bean的作用域相关

配置 bean 的时候有一个属性是 scope ,用来配置作用域,有五种可选值

  • singleton
    在 IOC 容器中,每一个 bean 定义对应一个对象实例,也就是单例 【默认】
  • prototype
    一个 bean 定义可以对应多个实例,并且在获取(getBean)的时候才会被实例化
  • request
    request 表示该针对每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP request 内有效
  • session
    session 作用域表示该针对每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效
  • global session
    类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义。
    Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。
    在 global session 作用域中定义的 bean 被限定于全局 portlet Session 的生命周期范围内。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。

然后获取 bean 的方法上面提到了一种,就是使用 ac 的 getBean 方法,除了这种还可以通过工厂的方式获得,但是不推荐使用,因为通过工厂获取会进行懒加载,也就是说当 get 的时候才会创建相应的实例

1
2
3
BeanFactory bf = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = (User) bf.getBean("user");
System.out.println(user);

bean的生命周期

在默认情况下,也就是当作用域是 singleton 的时候,bean 对象是单例的,这种情况生命周期相对复杂一点,下面的顺序

  1. 首先是实例化,当配置文件加载的时候,默认调用无参的构造函数
  2. 根据配置文件中的 property 进行属性注入,必须要有相应的 set 方法
  3. 如果 bean 对象实现了 BeanNameAware 接口,会调用其 setBeanName 方法,这个方法必须复写,通过这个方法可以获得配置的 id 名称
    和前面的 struts2 有点相似,实现这个接口可以获得配置的 id 名称
  4. 如果 bean 对象实现了 BeanFactoryAware 接口,会调用其 setBeanFactory 方法,通过这个方法可以获得 bean 工厂
  5. 如果 bean 对象实现了 ApplicationContextAware 接口,会调用其 setApplicationContext 方法,通过这个方法可以获得上下文对象
  6. 调用 BeanPostProcessor (后置处理器)的 postProcessBeforeInitialization 方法,如果配置了处理器的话
    后置处理器类似过滤器,每个 bean 实例化时(前、后)都要调用此处理器,有点像拦截器
    设置处理器需要建一个类实现 BeanPostProcessor 接口即可,再配置到 xml 文件中(一切皆是 bean)
  7. 如果 bean 对象实现了 InitializingBean 接口,会调用其 afterPropertiesSet 方法
  8. 如果在 xml 配置文件中的 bean 标签定义了 init-method 属性,那么会在这时调用其指定的方法
    使用这种方式耦合性比较低
  9. 调用 BeanPostProcessor (后置处理器)的 postProcessAfterInitialization 方法
  10. 然后就可以使用 bean 了!!
  11. 容器关闭….
  12. 如果 bean 对象实现了 DisposableBean 接口,这时会调用其 destroy 方法
  13. 如果在 xml 配置文件中的 bean 标签定义了 destroy-method 属性,那么会在这时调用其指定的方法
    使用这种方式耦合性比较低

如果不是用上下文获取的 bean,而是采用的工厂方法,那么就简单一些了,上面的5、6、9 就没有了
然后下面是两幅图,第一张比较详细,第二张比较简洁

bean生命周期(全).png

bean生命周期.png

使用注解指定

在配置文件中加入:<context:annotation-config/> 来启用注解
比如第八点自定义的 init 方法可以使用 @PostConstruct 注解来指定,不需要在 xml 文件中配置了
destroy 方法可以通过 @PreDestroy 注解来指定
更多注解的使用后面再说

装载bean

普通属性就不说了,主要是对于数组、集合的装填,首先看下配置的 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
26
27
28
29
30
31
32
33
34
35
<bean id="collectionTest" class="com.bfchengnuo.domain.CollectionTest">
<!--装载数组数据-->
<property name="strs">
<list>
<value>str1</value>
<value>str2</value>
<value>str3</value>
<value>str4</value>
</list>
</property>
<!--装载 List 数据,根据特性可以重复引用,并且是有顺序的-->
<property name="users">
<list>
<ref bean="user"/>
<ref bean="user2"/>
<ref bean="user"/>
<ref bean="user2"/>
</list>
</property>
<!--装载 Map 集合数据,根据特性不能有重复,后面会覆盖前面,当然只是 key 不同就行,无序-->
<property name="map">
<map>
<entry key="user" value-ref="user"/>
<entry key="user2" value-ref="user2"/>
<entry key="test" value-ref="user2"/>
</map>
</property>
<!--装填 set 集合-->
<property name="userSet">
<set>
<ref bean="user"/>
<ref bean="user2"/>
</set>
</property>
</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
private static void collectionTest() {
ApplicationContext ac = ApplicationContextUtil.getAc();
CollectionTest collectionTest = (CollectionTest) ac.getBean("collectionTest");

// 遍历数组
System.out.println("-----------数组遍历-----------");
for (String s : collectionTest.getStrs()) {
System.out.println(s);
}

// 遍历 List
System.out.println("-----------List遍历-----------");
for (User user : collectionTest.getUsers()) {
System.out.println(user.getName() + "::" + user.getAge());
}

// 遍历 Map
System.out.println("-----------Map遍历-----------");
for (Map.Entry<String, User> entry : collectionTest.getMap().entrySet()) {
System.out.println(entry.getKey() + "::" + entry.getValue().getName() + "-" + entry.getValue().getAge());
}

// 遍历 Set
System.out.println("-----------Set遍历-----------");
for (User user : collectionTest.getUserSet()) {
System.out.println(user.getName() + "::" + user.getAge());
}
}

property 标签中还可以继续再套一个 bean 标签,这样就相当于内部类,在其他地方就不能通过 ref 进行引用了

如果有继承关系可以使用 bean 的 parent 属性指定父 bean,配置的相关属性会被继承过来,当然你可以进行覆盖

如果需要设置值为空,可以使用 <null/> 标签

自动装配

使用 bean 标签中的 autowire 属性为一个 bean 定义指定自动装配模式,也就是说指定了自动装配就不需要注入对象了,它会自动根据设置去 IoC 容器中寻找合适的 bean 然后自动注入。但是并不推荐使用,仅作为了解;但是我看经常配合注解使用,尤其是在 SSH 的项目中

模式 描述
no 这是默认的设置,它意味着没有自动装配,你应该使用显式的bean引用来连线。你不用为了连线做特殊的事。
byName 由属性名自动装配。当找不到相应的连接关系时会尝试使用属性名去搜寻对应的 bean (id 和 name 相匹配);如果找不到或者找到多个则抛异常
byType 由属性数据类型自动装配。当找不到相应的连接关系时会尝试使用属性的类型搜寻对应的 bean (class 相匹配);如果找不到或者找到多个则抛异常
constructor 类似于 byType,但该类型适用于构造函数参数类型。如果在容器中没有一个构造函数参数类型的 bean,则一个致命错误将会发生。
autodetect Spring 首先尝试通过 constructor 使用自动装配来连接,如果它不执行,Spring 尝试通过 byType 来自动装配。

默认值也是可以设置的,在 beans 标签的 defualt-autowire 属性指定

分散配置

使用 Spring 的特殊 bean 来达到分散配置的目的,占位符使用 ${name} 的形式;前面说生命周期的时候说的那几个接口就是体现的这个功能
首先新建个 properties 文件,然后引入到配置文件中:

1
2
3
4
5
6
7
<!--分散配置,引入 prop 文件;如果引入多个文件可以用逗号分割-->
<context:property-placeholder location="temp.properties"/>
<!-- 可以使用占位符来引用 -->
<bean id="user3" class="com.bfchengnuo.domain.User">
<property name="name" value="${name}"/>
<property name="age" value="${age}"/>
</bean>

获取ApplicationContext

首先要明确的是 ApplicationContext 是个接口;其实它也是 BeanFactory 的子类,获取 ApplicationContext 实现类常用的有下面几种方式:

  1. ClassPathXmlApplicationContext 通过类路径,上面写过了,通常用在“桌面”系统开发
  2. FileSystemXmlApplicationContext 通过文件系统获取,需要指明绝对路径,用的很少
  3. XmlWebApplicationContext 通过 web 方式获取,传统项目这个用的还是比较多的
  4. AnnotationConfigApplicationContext 使用 Java 配置的方式来构建 ApplicationContext,例如 SpringBoot
  5. AnnotationConfigWebApplicationContext 使用 Java 配置方式的 Web 应用,可以在 web.xml 中指定。

AOP编程和IoC

AOP 和 IoC 是 Spring 的两大特点

AOP(Aspect Orient Programming),也就是面向切面编程。 可以这样理解,面向对象编程(OOP)是从静态角度考虑程序结构,面向切面编程(AOP)是从动态角度考虑程序运行过程。
这种在运行时,动态地将代码“切入”到类的指定方法、指定位置上的编程思想就是面向切面的编程。
或者说在不增加代码的基础上增加新的功能;面向(作用于)全部对象,或者一类对象(想一下学过的监听器、拦截器、过滤器)

下面就说说常见的几个术语:

  • 通知/增强(Advice)
    切面的工作被称为通知,通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只是在方法抛出异常时调用?
    简单说就是你想要(切入)的具体功能实现(想要干啥),比如安全,事物,日志操作等。
  • 连接点(JoinPoint)
    连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
    就是 Spring 允许你使用通知的地方
  • 切点(Pointcut)
    一组连接点的总称,用于指定某个通知(增强)应该在何时被调用。
    切入点是「在哪干」,你可能在很多地方(连接点)都可以干,但并不是每个地方都要干,要干的地方叫切点
  • 切面(Aspect)
    切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能
    通知说明了干什么和什么时候干(什么时候通过方法名中的before、after、around 等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。
  • 织入(weaving)
    把切面应用到目标对象来创建新的代理对象的过程。有3种方式,spring 采用的是运行时

Spring 中的通知有五种类型:

  1. 前置通知(Before):在目标方法被调用之前调用通知功能;[接口: MethodBeforeAdvice]
  2. 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;[接口: AfterReturningAdvice]
  3. 返回通知/最终通知(After-returning):在目标方法成功执行之后调用通知,如果有后置通知会在其之后执行;
  4. 异常通知(After-throwing):在目标方法抛出异常后调用通知;[接口: ThrowsAdvice]
  5. 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。[接口: MethodInterceptor]

运行顺序:前置通知/环绕通知目标方法执行返回通知/异常通知后置通知/环绕通知
这里需要注意的是:环绕通知由于和前置、后置处于同一个 aspect 内,所以是无法确定其执行顺序的,当然可以通过其他手段来解决
实际开发中,一般会将顺序执行的 Advice 封装到不同的 Aspect,然后通过注解或者实现接口的方式控制 Aspect 的执行顺序,二选一(对于在同一个切面定义的通知函数将会根据在类中的声明顺序执行)
关于 AOP 的更详细介绍:[1]:http://cometzb-xujun.iteye.com/blog/1537274;[2]:http://www.jianshu.com/p/66d21dae6a68


简单解释下什么是 IoC(控制反转 Inversion of Control),它是一种设计思想,和依赖注入有些相似之处(或者说不同名称,相同含义),就是说把创建对象和维护对象关系的控制权从程序中转移到了 Spring 容器中(applicationContext.xml),程序本身不再维护,换句话说就是从代码移到了配置文件中 ,对象的创建交给外部容器处理

你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制 ,所以说这个容器很重要,IoC 是有专门一个容器来创建这些对象,即由 Ioc 容器来控制对象的创建
简单总结:
控制反转解决对象的创建问题(交给别人创建),依赖注入解决在创建对象后,对象关系的处理问题(通过 set 方法依赖注入)

参考:http://luanxiyuan.iteye.com/blog/2279954

AOP快速入门

Spring 中“使用” AOP 的步骤一般是:

  1. 定义接口
  2. 编写对象(目标对象、被代理对象)
  3. 编写通知
  4. XML 文件中进行配置,包括被代理对象、通知、代理对象(ProxyFactoryBean)
  5. 通过代理对象进行使用

如果看过动态代理相关的知识,还是很好理解的,那就搞一个简单的前置通知来玩玩,前两部就省略了,很简单;主要是通知和 XML 的配置,这种 XML 的配置好像很 low 了,随便看看就好,稍后我会专门更新一篇新版的.

1
2
3
4
5
6
7
8
9
10
11
12
public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
/**
* 在被代理对象的前面执行
* @param method 方法对象--被调用方法的方法名
* @param objects 传入被调用方法的参数
* @param o 目标对象(被代理对象)
*/
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("Before...记录..." + method.getName());
}
}

然后是相关的配置文件,如果代理了多个接口记得要在 proxyInterfaces 都写进去,这样就可以在 getBean 后随意进行切换

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
<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="standardLoli" class="com.bfchengnuo.aop.StandardLoli">
<property name="flag" value="欧尼酱"/>
</bean>

<!--配置通知-->
<bean id="myMethodBeforeAdvice" class="com.bfchengnuo.aop.MyMethodBeforeAdvice"/>

<!--配置代理对象,Spring 提供-->
<bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--代理接口集-->
<property name="proxyInterfaces">
<list>
<value>com.bfchengnuo.aop.Loli</value>
</list>
</property>
<!--将通知织入代理对象,相当于把通知和代理对象建立关系-->
<property name="interceptorNames" value="myMethodBeforeAdvice"/>
<!--配置被代理对象-->
<property name="target" ref="standardLoli"/>
</bean>
</beans>

然后测试了下,还是挺不错的,如果之前写了多个接口,可以直接进行各种强转,当然你的通知要都实现了这些接口,终究还是多态的特性

1
2
3
4
5
6
7
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("com/bfchengnuo/aop/beans.xml");
Loli loli = (Loli) ac.getBean("proxyFactoryBean");
loli.hug();
System.out.println("----------------");
System.out.println(loli.speak("bfchengnuo"));
}

最后再来补充一个,就是定义切点,是自定义,也就是说指定某个通知在指定的方法切入,配置完后记得在代理对象中将通知换成这个就可以了

1
2
3
4
5
6
7
8
9
10
11
12
<!--自定义切入点-->
<bean id="myPointcutAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<!--设置的是前置通知的切入点-->
<property name="advice" ref="myMethodBeforeAdvice"/>
<!--设置在那个方法进行切入-->
<property name="mappedNames">
<list>
<!--是可以使用通配符的-->
<value>hug</value>
</list>
</property>
</bean>
喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~