Spring第一篇
Spring
1、简单概述
用了Spring这么久了,一直很想写一篇系列的文章来总结一下自己对Spring的理解。
在概括Spring之前,首先从Javaweb中总结下以前的web中写的代码,从这里入手来分析下使用spring的好处。当然这里可以不利用springmvc来做为基础框架,也可以自己来进行定义
下面按照MVC三层架构来写:
首先从controller层中入手:
public class HelloController {
private HelloService helloService;
public String hello(){
String result = helloService.hello();
return "hello";
}
}
再到service层:
public class HelloService {
private HelloDao helloDao;
public String hello() {
String result = helloDao.findById();
return result;
}
}
最终到dao
public class HelloDao {
public String findById() {
return "hello";
}
}
那么从上面不难看出,从controller层中使用到helloService对象,从service层中需要利用到helloDao对象。
在javaweb阶段的时候,这里采用的方式是直接new一个,其实这里也就是多例模式(接下来会讲),在使用完成之后,就立即释放,然后被JVM回收掉,涉及到垃圾回收的内容,这里不多赘述。
但是在这样的一种设计中,存在着一个最大的问题。那么就是controller强依赖于service
最常见的就是A类、B类、D类都依赖于C类,后期想要进行修改,比如说B类需要依赖于E类,那么就会对B类的源码进行修改,但是不能够轻易修改,因为可能会对依赖于B的类造成问题。
在这里放上一个链接,觉得讲解的不错:https://blog.csdn.net/qq_38157516/article/details/81979219
但是上面介绍的是初衷,理想化的状态。我们有时候又不得不去进行依赖,但是要求耦合性又没有原来那么高,尽量没有耦合性是最好的设计方式。
2、实现解耦
因为controller中强依赖于service,service又强依赖于dao。那么将会导致如果修改了service的代码,controller中也将会来进行修改,所以对于一个系统来说,最终可能会造成紊乱。
所以首先需要解决依赖问题。那么先自己来实现一个解耦合的案例:
service层:
public interface UserService {
void save();
}
public class UserServiceImpl implements UserService {
/**
* 存在编译期依赖:如果没有UserDaoImpl,代码编译是不通过的。
* 要避免编译期依赖,减少运行期依赖
* 解决思路:
* 1. 使用反射技术代替new
* 2. 提取配置文件
*/
//private UserDao userDao = new UserDaoImpl();
private UserDao userDao = (UserDao) BeanFactory.getBean("userDao");
@Override
public void save() {
userDao.save();
}
}
紧接着对BeanFactory来进行实现,这里才是核心!因为要在这里进行解耦实现
配置文件beans.properties
userDao=com.guang.dao.impl.UserDaoImpl
public class BeanFactory {
private static Map<String, Object> map = new HashMap<>();
static {
// 类加载时,读取properties文件,把其中所有的bean都创建对象,放到一个容器里
// 当需要使用时,直接从容器中获取即可
try {
//1.读取配置文件。专用于properties文件的
ResourceBundle bundle = ResourceBundle.getBundle("beans");
Enumeration<String> keys = bundle.getKeys();
while (keys.hasMoreElements()) {
String id = keys.nextElement();
String className = bundle.getString(id);
Class clazz = Class.forName(className);
Object object = clazz.newInstance();
map.put(id, object);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 对外暴露获取得到方法
public static Object getBean(String id){
return map.get(id);
}
}
从上面的代码中可以看出来,这里的解耦并非是完全的解耦,但是降低了耦合度。不需要按照原来的方式:
private UserDao userDao = new UserDaoImpl();
而是通过
private UserDao userDao = (UserDao) BeanFactory.getBean("userDao");
这样子来获取,那么即使UserDao中的代码发生了改变,只需要修改service中的一部分代码或者是再添加个接口即可。
如果使用到了其他的组件,声明下,再重新从容器中获取得到组件即可。
3、SpringIOC配置文件
首先需要了解一下spring中的基本的概念。
3.1、IOC
IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”。控制反转的意义就在于将创建对象的控制权让出去,而不是交给程序员来创建,交给了spring框架去创建。
3.2、DI
DI是Dependency Injection的缩写,也被翻译成是依赖注入。DI通常来说,是容器中的组件依赖了另外的一个组件,通过DI从容器中获取得到组件来进行依赖注入组建中的声明。
最简单的案例就是:service要被controller来依赖,但是controller可能不需要被其他的对象依赖。所以我觉得DI是IOC的一部分
3.3、组件
在spring中,习惯上将容器中的对象叫做组件
3.3、IOC和DI案例
使用环境:jdk8+maven+IDEA
第一步:引入依赖:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
第二步:controller和service
controller层:
public class HelloController {
private HelloService helloService;
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
public String hello(){
helloService.hello();
return "hello";
}
}
service层:
public interface HelloService {
String hello();
}
service实现层:
public class HelloServiceImpl implements HelloService {
@Override
public String hello() {
return "hello";
}
}
配置文件:bean.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="service.impl.HelloServiceImpl" id="helloService"/>
<bean class="controller.HelloController" id="helloController">
<property name="helloService" ref="helloService"/>
</bean>
</beans>
然后编写一个测试类来进行测试:
public class BeanFactoryTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:beans.xml");
HelloController helloController = (HelloController) applicationContext.getBean("helloController");
String hello = helloController.hello();
System.out.println("hello");
}
}
控制台输出一下:
hello
那么接下来结合着代码和配置文件解释下里面的内容:
public class HelloController {
// controller中依赖于HelloService,那么这个属性在使用的时候spring容器中应该要存在,所以要对将这个对象添加到容器中去
// 但是这个是接口,无法创建对象,那么就只能够找实现类了
private HelloService helloService;
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
public String hello(){
this.helloService.hello();
return "hello";
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="service.impl.HelloServiceImpl" id="helloService"/>
<!--这里是将HelloController注册到容器中去,id是唯一的,通过这个id就可以从容器中获取得到HelloController对象,默认属性也有很多:比如作用范围,初始化方法、销毁方法。没有指定作用范围,默认是singleton-->
<bean class="controller.HelloController" id="helloController">
<!--这里的property属性,表示的就是HelloController中的属性,ref表示的是引用类型的,引用的是上面注册到容器中的对象(组件)-->
<!--其实这里就是DI,依赖注入,因为HelloController中依赖了helloService,将上面的HelloService注入到HelloController的属性中去-->
<!--既然有引用,那么对应的就有普通类型的value-->
<property name="helloService" ref="helloService"/>
</bean>
</beans>
重点:表面上我们理解成Spring帮我们创建了两个对象,然后Controller依赖了Service,将Service注入进来;
那么这里既创建了对象,那么又解决了对象和对象之间的依赖关系。关系图如下:
在IOC阶段,肯定是先创建两个对象,然后创建过程中进行了初始化,发现HelloController依赖于HelloService,然后去创建HelloService对象,当HelloService对象创建好了之后,会有下面的图展示:
当Spring帮助我们创建好了对象之后,然后将依赖关系也给注入好了,那么这时候的对象才会真正的被放入到IOC容器中去;
所以其实在IOC阶段,组件并没有产生,因为不可用;只有当解决了DI以后,容器中才会有真正可用的组件。
因为spring容器只是会创建多例的对象,但是不会负责去维护这些多例对象,所以容器中都是单例的,所以只解决单例的依赖问题。如果创建的是多例的模式,Spring根据创建实例的方式(三种:无参构造、静态工厂非静态方法、静态工厂的静态方法)以及初始化方法来进行初始化
3.4、bean的实例化
bean组件首先要实例化后才能够放入到容器中,spring提供了几种bean实例化的方式
工厂非静态方法、工厂静态方法、无参构造方法(最常用)
工厂静态方法实例化:
public class StaticMethodFactory{
public static UserDao createUserDao(){
return new UserDaoImpl();
}
}
配置文件:
<bean id="userDao" class="com.guang.bean.StaticMethodFactory" factory-method="createUserDao"></bean>
配置文件中指明了使用StaticMethodFactory类中的createUserDao方法来进行实例化bean;
工厂非静态方法实例化:
public class NoStaticMethodFactory{
public UserDao createUserDao(){
return new UserDaoImpl();
}
}
配置文件:
<!-- 先配置工厂 -->
<bean id="noStaticMethodFactory" class="com.guang.factory.NoStaticMethodFactory"></bean>
<!-- 再配置UserDao -->
<!-- factory-bean是工厂bean的名字,使用的是工厂bean中的非静态方法createUserDao来创建出来的userDao组件 -->
<bean id="userDao" factory-bean="instanceFactory" factory-method="createUserDao"></bean>
3.5、常见注入方式
最常见的其实通过set方式注入,还有其他的,比如说构造方法注入、P标签,但是使用构造方法和P标签注入太过于麻烦,直接使用set方式进行注入即可。
在上面的案例中:
public class HelloController {
private HelloService helloService;
// 给属性提供了set方法,所以这里当然也是可以通过构造方法来进行注入
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
public String hello(){
helloService.hello();
return "hello";
}
}
但是spring的注解版的参数中并没有提供set方法,那么是通过暴力反射的方式来给参数注入;
4、SpringIOC注解版
注解版的在spring中可能并不是太过于明显,但是在springboot项目中,可以使用纯注解。
在spring中通常使用的是注解+配置文件的方式来使用。注解版的使用方便,但是也需要注意其使用原理。
4.1、IOC注解
常用注解:
注解 | 说明 |
---|---|
@Component |
用在类上,相当于bean标签 |
@Controller |
用在web层类上,配置一个bean(是@Component 的衍生注解) |
@Service |
用在service层类上,配置一个bean(是@Component 的衍生注解) |
@Repository |
用在dao层类上,配置一个bean(是@Component 的衍生注解) |
service层:
public interface UserService {
void save();
}
@Service("userService")//等价于@Conponent("userService")
public class UserServiceImpl implements UserService {
//注意:使用注解配置依赖注入时,不需要再有set方法
@Autowired
//@Qualifier("userDao")
private UserDao userDao;
@Override
public void save() {
userDao.save();
}
}
dao层:
public interface UserDao {
void save();
}
@Repository("userDao")//等价于@Component("userDao")
public class UserDaoImpl implements UserDao {
@Override
public void save() {
System.out.println("save.....method");
}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--开启组件扫描,将这个包下的所有了加上了能够成为bean的注解的类都添加成容器中的组件-->
<context:component-scan base-package="com.guang"/>
</beans>
4.2、DI常用注解
依赖注入的注解
注解 | 说明 |
---|---|
@Autowired |
相当于property标签的ref,spring容器通过类型进行注入 |
@Qualifier |
结合@Autowired 使用,用于根据名称注入依赖 |
@Resource |
相当于@Autowired + @Qualifier ,JDK提供的 |
@Value |
相当于property标签的value |
如果按照类别(@Autowired)进行注入的话,最常见的就是controller中使用的是属性的数据类型是接口,那么这个时候应该注意,如果有多个类型的组件,那么
就需要使用到@Qualifier来知名使用具体的组件的名字来进行依赖注入。
5、bean的生命周期以及作用范围
bean是通过实例化方式来进行创建的,实例化之后,需要进行初始化,最终还需要被销毁。
5.1、bean的作用范围
bean的作用范围在配置文件中是以scope属性来进行配置的。
- scope属性取值如下:
取值 | 说明 |
---|---|
singleton |
默认,表示单例的,一个Spring容器里,只有一个该bean对象 |
prototype |
多例的,一个Spring容器里,有多个该bean对象 |
request |
web项目里,Spring创建的bean对象将放到request 域中:一次请求期间有效 |
session |
web项目里,Spring创建的bean对象将放到session 域中:一次会话期间有效 |
globalSession |
web项目里,应用在Portlet环境/集群环境 |
-
不同scope的bean,生命周期:
-
singleton:bean的生命周期和Spring容器的生命周期相同
- 整个Spring容器中,只有一个bean对象
- 何时创建:加载Spring配置文件,初始化Spring容器时,bean对象创建
- 何时销毁:Spring容器销毁时,bean对象销毁
-
prototype:bean的生命周期和Spring容器无关。Spring创建bean对象之后,交给JVM管理了
- 整个Spring容器中,会创建多个bean对象,创建之后由JVM管理
-
-
何时创建:调用
getBean
方法获取bean对象时,bean对象创建- 何时销毁:对象长时间不用时,GC进行垃圾回收
5.2、生命周期
spring提供了两种方式来进行初始化:
init-method
:指定类中初始化方法名称,该方法将在bean对象被创建时执行destroy-method
:指定类中销毁方法名称,该方法将在bean对象被销毁时执行
但是我们一般不会使用这种方式,在springboot我们会实现一个接口:
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
在afterPropertiesSet中来对bean做初始化
比如:
@Component
public class User implements InitializingBean {
private Integer age;
private String username;
// 没有提供set/get方法
@Override
public void afterPropertiesSet() throws Exception {
this.age = 12;
this.username = "guang";
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", username='" + username + '\'' +
'}';
}
}
测试类中:
public class BeanFactoryTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:beans.xml");
User user = applicationContext.getBean(User.class);
System.out.println(user.toString());
}
}
但是最终在控制台显示的是:
User{age=12, username='guang'}
说明了实现了这个接口的,不需要来提供set方法给属性进行赋值,注解版的同样如此。
关于销毁方法,也提供了一个接口:
public interface DisposableBean {
void destroy() throws Exception;
}
案例:
@Component
@Scope(value = "prototype")
public class User implements InitializingBean, DisposableBean {
private Integer age;
private String username;
// 没有提供set/get方法
@Override
public void afterPropertiesSet() throws Exception {
this.age = 12;
this.username = "guang";
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", username='" + username + '\'' +
'}';
}
@Override
public void destroy() throws Exception {
System.out.println("销毁对象之前需要调用的方法");
}
}
测试方法:
public class BeanFactoryTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:beans.xml");
User user = applicationContext.getBean(User.class);
System.out.println(user.toString());
System.out.println("IOC容器准备关闭");
applicationContext.close();
}
}
但是控制台显示:
User{age=12, username='guang'}
IOC容器准备关闭
但是我们的销毁方法并没有执行!查了下资料,如果是多例的话,并不会自动调用,而是因为多例的对象不由IOC容器来进行管理。如果是单例的组件,那么就会来调用这个方法,那么测试一下:
@Component
// 默认就是单例
public class User implements InitializingBean, DisposableBean {
private Integer age;
private String username;
// 没有提供set/get方法
@Override
public void afterPropertiesSet() throws Exception {
this.age = 12;
this.username = "guang";
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", username='" + username + '\'' +
'}';
}
@Override
public void destroy() throws Exception {
System.out.println("销毁对象之前需要调用的方法");
}
}
控制台输出:
User{age=12, username='guang'}
IOC容器准备关闭
销毁对象之前需要调用的方法
5.3、线程安全问题
前面在并发包专题也介绍了这个问题,问题是先找到多线程的入口在哪里。tomcat服务器在接收到client的请求之后,每个请求就是一个线程,所以在达到我们能够处理的地方的时候,就已经产生了多线程。比如说filter、controller中已经有了多线程。我们可以在filter中使用ThreadLocal等给线程绑定数据等等,常规操作。
所以,也就说明了在controller、service中面临着线程安全问题,所以尽量不要在类中成员变量中定义变量。
但是我们在controller、service中会注入springIOC容器中的对象,因为默认是单例的,所以尽量操作方法,而不是需要操作变量。因为对于多线程来说,每个方法都会在自己线程的栈空间中执行,而唯一能够产生线程安全问题的就是这些个数据。所以要注意这些数据的使用。
我们通常的使用方式就是在业务层service中定义一把锁:
private final ReentrantLock lock = new ReentrantLock();
我们使用其中的lock和unlock方法来做线程安全处理。保证在某一个时间段内,只有一个线程可以操作。
这种方式操作起来比较方便,当然也可以使用syncronized关键字,因为对象是单例的,所以也可以使用this关键字。但是通常来说都是利用了lock锁来解决这种问题。
可以利用这种方式来操作缓存等,比较方便!
6、总结
从传统web方式出发到spring框架,最直观的感觉就是spring利用IOC来帮助我们解决了解耦问题,其实这也是最直观的方式。将对象的创建都交给了SpringIOC,让spring来管理这些组件,需要使用到组件的地方就直接从容器中来进行获取,非常方便。
对象的创建交给了Spring容器,我们在使用的时候需要通过DI来进行注入即可,但是创建组件是利用了对应的注解,需要扫描到之后添加到Spring容器中去。
那么又有了组件的生命周期和作用域问题以及带来的线程安全问题。
所以编程主线是:
组件所属类------->成为组件注解----------->生命周期-------->作用范围---------->DI---------->线程安全
注意bean的生命周期方法和作用范围
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?