1,Spring概述
1.1,Spring简介
Spring是一个从实际开发中抽取出来的框架,因此它完成了大量开发中的通用步骤,留给开发者的仅仅是与特定应用相关的部分,从而大大提高了企业应用的开发效率。
Spring为企业应用的开发提供了一个轻量级的解决方案。该方案包括:基于依赖注入的核心机、基于AOP的声明式事务管理、与多种持久层技术的整合,以及优秀的Web MVC框架等。Spring致力于JavaEE应用各层的解决方案,而不是仅仅专注于某一层的方案。可以说:Spring是企业应用开发的“一站式选择”,Spring贯穿表现层、业务层、持久层。然而,Spring并不想取代那些已有的框架,而是以高度的开放性与他们无缝整合。
Spring的核心是个轻量级(Lightweight)的容器(Container),它是实现IOC(InversionOfControl)容器和非侵入式(NoIntrusive)的框架,并提供AOP(Aspect-oriented programming)概念的实现方式;提供对持久层、事务的支持;提供MVC Web框架的实现,并对一些常用的企业服务API提供一致的模型封装,是一个全方位的应用程序框架,对于现存的各种框架,Spring也提供与他们相整合的方案。
为何要使用Spring:
- 降低组件之间的耦合度,实现软件各层之间的解耦。
- 可以使用容器提供的众多服务,如:事务管理服务、消息服务等等。当我们使用容器管理事务时,开发人员就不再需要手工控制事务.也不需处理复杂的事务传播。
- 容器提供单例模式支持,开发人员不再需要自己编写实现代码。
- 容器提供了AOP技术,利用它很容易实现如权限拦截、运行期监控等功能。
- 容器提供的众多辅作类,使用这些类能够加快应用的开发,如: JdbcTemplate、 HibernateTemplate。
- Spring对于主流的应用框架提供了集成支持,如:集成Hibernate、JPA、Struts等,这样更便于应用的开发。
Spring的优点如下:
- 低侵入式设计,代码的污染极低。
- 独立于各种应用服务器,基于Spring框架的应用,可以真正实现Write Once,Run Anywhere的承诺。
- Spring的IoC容器降低了业务对象替换的复杂性,提高了组件之间的解耦。
- Spring的AOP支持允许将一些通用的任务如安全、事务、日志等进行集中式处理,从而提供了更好的复用。
- Spring的ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问。
- Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可以自由选用Spring框架的部分或全部。
1.2,Spring的组成
Spring框架由7个核心模块组成,每个模块都可以单独的工作,或者与其他模块联合工作,每个模块的功能如下:
- 核心容器模块:核心容器提供Spring框架的基本功能。核心容器的主要组件是BeanFactory, 它是工厂模式的实现。BeanFactory使用控制反转(IOC)模式将应用程序的配置和依赖性规范从实际的应用程序代码中分开 。
- Spring 上下文模块:这是一个配置文件,向Spring框架提供上下文信息。Spring上下文包括企业服务,例如:JNDI、EJB、JavaMail、国际化、校验等功能。
- Spring AOP模块:通过配置文件管理,将面向切面编程的功能集成到了Spring框架中,可以很容易的使用Spring框架管理任何对象支持AOP。Spring AOP模块为基于Spring的应用程序中的对象提供了事务管理服务。
- Spring DAO模块:JDBC DAO用来管理异常处理和不同数据库厂商抛出的异常,简化了错误处理,并且极大的降低了要编写的异常代码的数量。Spring DAO面向JDBC的异常遵循通用的DAO异常处理过程。
- Spring ORM 模块:Spring框架插入了若干个ORM框架,从而提供了ORM的对象的关系映射功能.其中包括:JDO、Hibernate等。
- Spring Web模块:Web模块建立在程序的上下文模块之上,为基于Web的应用程序提供了 上下文。Spring框架支持与Struts的集成。
- Spring MVC 模块:MVC框架是一个全功能的构建Web应用程序的MVC的实现。
1.3,Spring的下载
Spring是一个独立的框架,它不需要依赖任何Web服务器或容器,它即可在独立的JavaSE项目中使用,当然也可以在Java Web项目中使用。
1,下载Spring项目
登录Spring站点(JFrog),找到最最新最稳定的(带RELEASE后缀的),然后下载dist后缀项目(spring-5.2.9.RELEASE-dist.zip )。
另外,Spring核心容器必须依赖于common-logging的JAR包,从Maven Central Repository Search下载commons-logging工具。
当使用Spring框架时,必须使用Spring Core Container(Spring容器),它代表了Spring框架的核心机制,Spring Core Container主要由org.springframework.core、org.springframework.beans、org.springframework.context和org.springframework.expression四个包及其子类包组成,主要提供Spring IoC支持。其中org.springframework.expression及其子包是Spring3.0新增的,它提供了Spring Expression Language支持。
Spring框架的两个最基本和最重要的包是org.springframework.beans.factory(该包中的主要接口是BeanFactory)和org.springframework.context包(该包中的主要接口是ApplicationFactory)。这两个包中的代码提供了Spring IoC特性的基础。
2,下载内容
- docs:该文件夹下存放Spring的相关文档,包含开发指南、API参考文档。
- libs:该目录下的JAR包分三类:(1)Spring框架class文件的JAR包。(2)Spring框架源文件的压缩包,文件名以-sources结尾。(3)Spring框架API文档的压缩包,文件名以-javadoc结尾。
- schemas:该目录下包含了Spring各种配置文档的XML Schema文档。
- readme.txt、notice.txt、license.txt:说明文档。
1.4,IDEA创建Spring项目
1,创建Java项目,一路默认,输入项目名称。
2,添加框架支持
3,添加Spring核心jar包
4,添加到WEB-INF
2,使用Spring管理Bean
Spring核心容器就是一个超级工厂,所有的对象都会被当成Spring核心容器管理的对象,所以一切对象统称为Bean。
2.1,创建Bean
package Bean;public class UserInfo {private Integer id;private String name;private String address;private Integer age;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}}
使用Spring框架后,Spring核心容器是整个应用中的超级工厂,所有的Java对象都交给Spring容器管理——这些Java对象被统称为Spring容器中的Bean。
现在需要解决的问题是:Spring容器怎么知道管理那些Bean呢?答案是XML配置文件(也可以用注解),Spring使用XML配置文件来管理容器中的Bean。因此,接下来为该项目增加XML配置文件,Spring对XML配置文件的文件名没有任何要求,可以随意。
2.2,创建配置文件
Spring框架的本质是通过XML配置来驱动Java代码,这样就实现了系统中各组件的解耦。Bean的配置文件是一个XML文件,它可命名为beans.xml、applicationContext.xml或其它。一般习惯使用applicationContext.xml。
<beans.../>元素是Spring配置文件的根元素,该元素的属性:
- default-lazy-init:指定该<benas.../>元素下配置的所有Bean默认的延迟行为;此参数表示延时加载,即在项目启动时不会实例化注解的bean,除非启动项目时需要用到,未实例化的注解对象在程序实际访问调用时才注入调用。
- default-merge:指定该<beans.../>元素下配置的所有Bean默认的merge的行为
- default-autowire:指定该<beans.../>元素下配置的所有Bean默认的自动装配行为
- default-autowire-candidates:指定该<beans.../>元素下配置的所有Bean默认是否作为自动装配的候选Bean
- default-init-mothod:指定该<beans.../>元素下配置的所有Bean默认的初始化方法(参数值:方法名)
- default-destroy-method:指定该<beans.../>元素下配置的所有Bean默认的回收方法(参数值:方法名)
<beans.../>的所有属性都可以在每个<bean.../>子元素中指定,区别是为<bean.../>指定属性只对特定Bean起作用;在<beans.../>元素指定的属性对<beans.../>包含的所有Bean都起作用。当二者所指定的属性不一致时,<bean.../>下指定的属性会覆盖<beans.../>下指定的属性。在同一个文件中<bean />里面设置的优先级大于<beans />里设置的优先级。<beans.../>元素可以包含多个<bean.../>子元素,每个<bean.../>子元素定义一个Bean,每个Bean对应Spring容器里的一个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"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userInfoBean" class="Bean.UserInfo"><property name="address" value="郑州市文化路32号"></property><property name="age" value="18"></property><property name="name" value="燕双嘤"></property><property name="id" value="1"></property></bean> </beans>
上面配置文件很简单,该配置文件的根元素是<beans/>,根元素主要就是包括多个<bean/>元素,每个<bean/>元素定义了一个Bean。驱动spring以反射方式调用该类无参的构造器:
- id:确定该Bean唯一标识。容器对Bean的管理、访问,以及该Bean的依赖关系,都通过该属性完成。Bean的id属性在Spring中是唯一的。
- class:指定该Bean的具体实现类,不能是接口。Spring容器必须是知道创建Bean的实现类,而不能是接口。在通常情况下,Spring会直接使用new关键字创建该Bean的实例,因此必须提供Bean实现类的类名。
- name:Bean的别名。还可以通过<alias.../>标签指定别名
<alias name="person" alias="燕双嘤"/>
Spring可以把”一切Java对象“当成容器中的Bean,因此不管该Java类是JDK提供的,还是第三方框架提供的,抑或是开发者自己实现的,只要是Java类,并将它配置在XML配置文件中,Spring容器就可以管理它。
实际上,配置文件中的<bean/>元素默认反射方式来调用该类的无参构造器:
<bean id="userInfoBean" class="Bean.UserInfo">
Spring框架解析该<bean/>元素后将可以得到两个字符串,其中idStr的值为“person”(解析<beans/>元素的id属性得到的值),classStr的值为"Bean.UserInfo"(解析<bean/>元素的class属性得到的值)。
String idStr = ...; //解析<bean.../>元素id属性得到字符串值为UserInfo Stiring classStr = ...; //解析<bean.../>元素class属性得到该字符串的值 Class clazz = Class.forName(classStr); Object obj = clazz.newInstance(); container.put(idStr,obj); //container代表Spring容器
Spring框架会通过反射根据<bean/>元素的class属性指定的类名创建了一个Java对象,并以<bean...>元素的id属性的值为key,将该对象放入Spring容器中——这个Java对象就成为了Spring容器中的Bean。
因为反射,在Spring配置文件中配置Bean时,class属性的值必须是Bean实现类的完整类名(必须带包名),不能是接口,不能是抽象类(除非有特殊配置)。否则Spring无法使用反射创建该类的实例。
上面配置文件还包括一个<property.../>子元素,<property.../>子元素通常用于作为<bean/>元素的子元素,它驱动Spring在底层反射执行一次setter方法。其中<property.../>的name属性值决定执行哪个setter方法,而value或ref决定执行setter方法的传入参数。
- 如果传入参数是基本类型及其包装类,String等类型,则使用value属性指定传入参数。
- 如果以容器中其他Bean作为参数,则使用ref属性指定传入参数。
Spring框架只要看到<property.../>子元素,Spring框架就会在底层以反射的方式执行一次setter方法。该Bean一旦创建处理,Spring会立即根据<property.../>子元素来执行setter方法。也就是说,<bean/>元素驱动Spring调用构造器创建对象;<property/>子元素驱动Spring执行setter方法,这两步先后执行,中间几乎没有任何间隔。
2.3,创建测试类
接下来程序就可以通过Spring容器来访问容器中的Bean,ApplicationContext是String容器最常用的接口,该接口有下面两个实现类:
- ClassPathXmlApplicaiontContext:从类加载路径下搜索配置文件,并根据配置文件来创建Spring容器。
- FileSystemXmlApplicationContext:从文件系统的相对路径或绝对路径下去搜索配置文件,并根据配置文件来创建Spring容器。
对于Java项目而言,类加载路径总是稳定的,因此通常是使用ClassPathXmlApplicationContext创建Spring容器。
package Action;import java.io.FileNotFoundException; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import Bean.UserInfo;public class Test {public static void main(String[] args) throws BeansException, FileNotFoundException {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");UserInfo userInfo = (UserInfo) context.getBean("userInfoBean");System.out.println(userInfo.getName() + ":" + userInfo.getAddress());} }
Spring获取Bean对象主要有两个方法:
- Object getBean(String id):根据容器中Bean的id来获取指定Bean,获取Bean之后需要进行强制类型转换。
- T getBean(String name, Class<T> requiredType):根据容器中Bean的id来获取指定Bean,但该方法带一个泛型参数,因此获取取Bean之后无须进行强制类型转换。
获取Bean(Java对象)之后,即可通过该对象来调用方法、访问实例变量(如果访问权限允许)——原来怎么使用Java对象,现在仍然怎么用。
使用Spring框架最大的改变之一是:程序不再使用new调用构造器创建Java对象,所有的Java对象都由Spring容器负责创建。
3,核心问题
3.1,Spring框架中的设计模式
- 代理模式—在 AOP(JDK动态代理和CGLIB)。
- 单例模式—在 spring 配置文件中定义的 bean 默认为单例模式(ApplicationContext源码)。
- 工厂模式—IOC原理,BeanFactory 用来创建对象的实例。Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
- 观察者模式—ApplicationContext的事件机制、Spring中Observer模式常用的地方是listener的实现。如ApplicationListener。
- 策略模式—Spring的事务机制。
3.2,Spring Bean 默认是单例的,高并发情况下,如何保证并发安全?
https://shao12138.blog.csdn.net/article/details/113058359#t2
https://shao12138.blog.csdn.net/article/details/113058359#t2Spring的bean默认都是单例的,某些情况下,单例是并发不安全的,以Controller举例,问题根源在于,我们可能会在Controller中定义成员变量,如此一来,多个请求来临,进入的都是同一个单例的Controller对象,并对此成员变量的值进行修改操作,因此会互相影响,无法达到并发安全(不同于线程隔离的概念,后面会解释到)的效果。
@Controller public class HomeController {private int i;@GetMapping("testsingleton1")@ResponseBodypublic int test1() {return ++i;} }
多次访问此url,可以看到每次的结果都是自增的,所以这样的代码显然是并发不安全的。
【单例变原型】对web项目,可以Controller类上加注解@Scope("prototype")或@Scope("request"),对非web项目,在Component类上添加注解@Scope("prototype")。
优点:实现简单;
缺点:很大程度上增大了bean创建实例化销毁的服务器资源开销。
【线程隔离类ThreadLocal】我们尝试将成员变量包装为ThreadLocal,以试图达到并发安全,同时打印出Http请求的线程名,修改代码如下:
@Controller public class HomeController {private ThreadLocal<Integer> i = new ThreadLocal<>();@GetMapping("testsingleton1")@ResponseBodypublic int test1() {if (i.get() == null) {i.set(0);}i.set(i.get().intValue() + 1);log.info("{} -> {}", Thread.currentThread().getName(), i.get());return i.get().intValue();} }
[INFO ] 2019-12-03 11:49:08,226 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-1 -> 1 [INFO ] 2019-12-03 11:49:16,457 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-2 -> 1 [INFO ] 2019-12-03 11:49:17,858 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-3 -> 1 [INFO ] 2019-12-03 11:49:18,461 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-4 -> 1 [INFO ] 2019-12-03 11:49:18,974 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-5 -> 1 [INFO ] 2019-12-03 11:49:19,696 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-6 -> 1 [INFO ] 2019-12-03 11:49:22,138 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-7 -> 1 [INFO ] 2019-12-03 11:49:22,869 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-9 -> 1 [INFO ] 2019-12-03 11:49:23,617 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-8 -> 1 [INFO ] 2019-12-03 11:49:24,569 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-10 -> 1 [INFO ] 2019-12-03 11:49:25,218 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-1 -> 2 [INFO ] 2019-12-03 11:49:25,740 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-2 -> 2 [INFO ] 2019-12-03 11:49:43,308 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-3 -> 2 [INFO ] 2019-12-03 11:49:44,420 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-4 -> 2 [INFO ] 2019-12-03 11:49:45,271 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-5 -> 2 [INFO ] 2019-12-03 11:49:45,808 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-6 -> 2 [INFO ] 2019-12-03 11:49:46,272 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-7 -> 2 [INFO ] 2019-12-03 11:49:46,489 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-9 -> 2 [INFO ] 2019-12-03 11:49:46,660 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-8 -> 2 [INFO ] 2019-12-03 11:49:46,820 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-10 -> 2 [INFO ] 2019-12-03 11:49:46,990 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-1 -> 3 [INFO ] 2019-12-03 11:49:47,163 com.cjia.ds.controller.HomeController.test1(HomeController.java:50) http-nio-8080-exec-2 -> 3 ......
从日志分析出,二十多次的连续请求得到的结果有1有2有3等等,而我们期望不管我并发请求有多少,每次的结果都是1;同时可以发现web服务器默认的请求线程池大小为10,这10个核心线程可以被之后不同的Http请求复用,所以这也是为什么相同线程名的结果不会重复的原因。ThreadLocal的方式可以达到线程隔离,但还是无法达到并发安全。
【尽量避免使用成员变量】单例bean的成员变量这么麻烦,能不用成员变量就尽量避免这么用,在业务允许的条件下,将成员变量替换为RequestMapping方法中的局部变量,多省事。这种方式自然是最恰当的。代码修改如下:
@Controller public class HomeController {@GetMapping("testsingleton1")@ResponseBodypublic int test1() {int i = 0;// TODO biz codereturn ++i;} }
但当很少的某种情况下,必须使用成员变量呢,我们该怎么处理?
【使用并发安全的类】Java作为功能性超强的编程语言,API丰富,如果非要在单例bean中使用成员变量,可以考虑使用并发安全的容器,如ConcurrentHashMap、ConcurrentHashSet等,将我们的成员变量(一般可以是当前运行中的任务列表等这类变量)包装到这些并发安全的容器中进行管理即可。
【分布式或微服务的并发安全】如果还要进一步考虑到微服务或分布式服务的影响,【使用并发安全的类】便不足以处理了,所以可以借助于可以共享某些信息的分布式缓存中间件如Redis等,这样即可保证同一种服务的不同服务实例都拥有同一份共享信息(如当前运行中的任务列表等这类变量)。