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的生命周期方法和作用范围

posted @   雩娄的木子  阅读(71)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示