Spring框架技术

点击查看源网页

Spring框架技术

SSM(Spring+SpringMVC+Mybatis)阶段的学习,也算是成功出了Java新手村。

前面我们已经学习过Mybatis了。

从这里开始,很多的概念理解起来就稍微有一点难度了,因为你们没有接触过企业开发场景,很难体会到那种思想带来的好处,甚至到后期接触到的几乎都是基于云计算和大数据理论实现的框架(当下最热门最前沿的技术)逐渐不再是和计算机基础相关联,而是和怎么高效干活相关了。

在JavaWeb阶段,我们已经学习了如何使用Java进行Web应用程序开发,我们现在已经具有搭建Web网站的能力,

但是,我们在开发的过程中,发现存在诸多的不便,我们发现虽然我们思路很清晰,知道如何编写对应的接口,但是这样的开发效率,实在是太慢了,并且对于对象创建的管理,存在诸多的不妥之处,因此,我们要去继续学习更多的框架技术,来简化和规范我们的Java开发。

Spring就是这样的一个框架(文档:https://docs.spring.io/spring-framework/docs/5.2.13.RELEASE/spring-framework-reference/),它就是为了简化开发而生。

它是轻量级的IoCAOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器,并且它的生态已经发展得极为庞大。

那么,首先一问,什么是IoCAOP,什么又是JavaBean呢?

只是听起来满满的高级感,实际上没有多高级(很多东西都是这样,名字听起来很牛,实际上只是一个很容易理解的东西)

因此,一切的一切,我们还要从JavaBean说起。

什么是JavaBean

JavaBean就是有一定规范的Java实体类,跟普通类差不多,不同的是类内部提供了一些公共的方法以便外界对该对象内部属性进行操作,比如set、get操作,实际上,就是我们之前一直在用的:

public class User{
	private String name;
	private int age;
	public String getName(){
		return name;
	}
	public String getAge(){
		return age;
	}
	public void setName(String name){
		this.name = name;
	}
	public void setAge(int age){
		this.age = age;
	}
}

它的所有属性都是private,所有的属性都可以通过get/set方法进行访问,同时还需要有一个无参构造(默认就有)

因此我们之前编写的很多类,其实都可以是一个JavaBean。

IoC理论基础

整个程序其实是依靠各个部分相互协作,共同完成一个操作。

比如要展示借阅信息列表,那么首先需要使用Servlet进行请求和响应的数据处理,然后请求的数据全部交给对应的Service(业务层)来处理,当Service发现要从数据库中获取数据时,再向对应的Mapper发起请求。

它们之间就像连接在一起的齿轮,谁也离不开谁:

img

就像一个团队,每个人的分工都很明确,流水线上的一套操作必须环环相扣,这是一种高度耦合的体系。

虽然这样的体系逻辑非常清晰,整个流程也能够让人快速了解,但是这样存在一个很严重的问题,我们现在的时代实际上是一个软件项目高速迭代的时代,我们发现很多App三天两头隔三差五地就更新,而且是什么功能当下最火,就马不停蹄地进行跟进开发。

因此,就很容易出现,之前写好的代码,实现的功能,需要全部推翻,改成新的功能,那么我们就不得不去修改某些流水线上的模块,但是这样一修改,会直接导致整个流水线的引用关系大面积更新。

就像我不想用这个Service实现类了,我想使用其他的实现类用不同的逻辑做这些功能,那么这个时候,我们只能每个类都去挨个进行修改,当项目特别庞大时,光是改个类名就够你改一天。

因此,高耦合度带来的缺点是很明显的,也是现代软件开发中很致命的问题。如果要改善这种情况,我们只能将各个模块进行解耦,让各个模块之间的依赖性不再那么地强。

也就是说,Service的实现类,不再由我们决定,而是让程序自己决定,所有的实现类对象,全部交给程序来管理,所有对象之间的关系,也由程序来动态决定,这样就引入了IoC理论。

IOC是Inversion of Control的缩写,翻译为:“控制反转”,把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。

img

我们可以将对象交给IoC容器进行管理。

比如当我们需要一个接口的实现时,由它根据配置文件来决定到底给我们哪一个实现类,这样,我们就可以不用再关心我们要去使用哪一个实现类了,我们只需要关心,给到我的一定是一个可以正常使用的实现类,能用就完事了,反正接口定义了啥,我只管调,这样,我们就可以放心地让一个人去写视图层的代码,一个人去写业务层的代码,开发效率更高。

高内聚,低耦合,是现代软件的开发的设计目标,而Spring框架就给我们提供了这样的一个IoC容器进行对象的管理。

使用IoC容器

首先一定要明确,使用Spring首要目的是为了使得软件项目进行解耦,而不是为了去简化代码!

Spring并不是一个独立的框架,它实际上包含了很多的模块:

点击查看源网页

而我们首先要去学习的就是Core Container,也就是核心容器模块。

Spring是一个非入侵式的框架,就像一个工具库一样,因此,我们只需要直接导入其依赖就可以使用了。

第一个Spring项目

我们创建一个新的Maven项目,并导入Spring框架的依赖,Spring框架的坐标:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.13</version>
</dependency>

接着在resource中创建一个Spring配置文件,命名为test.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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

最后,在主方法中编写:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("text");
    
}

这样,一个最基本的Spring项目就创建完成了,接着我们来看看如何向IoC容器中注册JavaBean,首先创建一个Student类:

//注意,这里还用不到值注入,只需要包含成员属性即可,不用Getter/Setter。
public class Student {
    String name;
    int age;
}

然后在配置文件中添加这个bean:

<bean name="student" class="com.test.bean.Student"/>

现在,这个对象不需要我们再去生成了,而是由IoC容器来提供:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = (Student) context.getBean("student");
    System.out.println(student);
}

实际上,这里得到的Student对象是由Spring通过反射机制帮助我们创建的,初学者会非常疑惑,为什么要这样来创建对象,我们直接new一个它不香吗?

为什么要交给IoC容器管理呢?在后面的学习中,我们再慢慢进行体会。

将JavaBean交给IoC容器管理

通过前面的例子,我们发现只要将我们创建好的JavaBean通过配置文件编写,即可将其交给IoC容器进行管理,那么,我们来看看,一个JavaBean的详细配置:

<bean name="student" class="com.test.bean.Student"/>

其中name属性(也可以是id属性),全局唯一,不可出现重复的名称,我们发现,之前其实就是通过Bean的名称来向IoC容器索要对应的对象,也可以通过其他方式获取。

我们现在在主方法中连续获取两个对象:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
Student student = (Student) context.getBean("student");
Student student2 = (Student) context.getBean("student");
System.out.println(student);
System.out.println(student2);

我们发现两次获取到的实际上是同一个对象,也就是说.

默认情况下,通过IoC容器进行管理的JavaBean是单例模式的,无论怎么获取始终为那一个对象,那么如何进行修改呢?只需要修改其作用域即可,添加scope属性:

<bean name="student" class="com.test.bean.Student" scope="prototype"/>

通过将其设定为prototype(原型模式)来使得其每次都会创建一个新的对象。

我们接着来观察一下,这两种模式下Bean的生命周期,我们给构造方法添加一个输出:

public class Student {
    String name;
    int age;

    public Student(){
        System.out.println("我被构造了!");
    }
}

接着我们在mian方法中打上断点来查看对象分别是在什么时候被构造的。

我们发现,当Bean的作用域为单例模式,那么它会在一开始就被创建,而处于原型模式下,只有在获取时才会被创建,也就是说。

单例模式下,Bean会被IoC容器存储,只要容器没有被销毁,那么此对象将一直存在,而原型模式才是相当于直接new了一个对象,并不会被保存。

我们还可以通过配置文件,告诉创建一个对象需要执行此初始化方法,以及销毁一个对象的销毁方法:

public class Student {
    String name;
    int age;

    private void init(){
        System.out.println("我是初始化方法!");
    }

    private void destroy(){
        System.out.println("我是销毁方法!");
    }

    public Student(){
        System.out.println("我被构造了!");
    }
}
public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = (Student) context.getBean("student");
    System.out.println(student);
    context.close();  //手动销毁容器
}

最后在XML文件中编写配置:

<bean name="student" class="com.test.bean.Student" init-method="init" destroy-method="destroy"/>

接下来测试一下即可。

我们还可以手动指定Bean的加载顺序,若某个Bean需要保证一定在另一个Bean加载之前加载,那么就可以使用depend-on属性。

依赖注入DI

现在我们已经了解了如何注册和使用一个Bean,那么,如何向Bean的成员属性进行赋值呢?

也就是说,IoC在创建对象时,需要将我们预先给定的属性注入到对象中,非常简单,我们可以使用property标签来实现,但是一定注意,此属性必须存在一个set方法,否则无法赋值:

<bean name="student" class="com.test.bean.Student">
    <property name="name" value="小明"/>
</bean>
public class Student {
    String name;
    int age;

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

    public void say(){
        System.out.println("我是:"+name);
    }
}

最后测试是否能够成功将属性注入到我们的对象中:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = (Student) context.getBean("student");
    student.say();
}

那么,如果成员属性是一个非基本类型非String的对象类型,我们该怎么注入呢?

public class Card {
}
public class Student {
    String name;
    int age;
    Card card;

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

    public void say(){
        System.out.println("我是:"+name+",我都学生证:"+card);
    }
}

我们只需要将对应的类型也注册为bean即可,然后直接使用ref属性来进行引用:

<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student">
    <property name="name" value="小明"/>
    <property name="card" ref="card"/>
</bean>

那么,集合如何实现注入呢?我们需要在property内部进行编写:

<bean name="student" class="com.test.bean.Student">
    <property name="list">
        <list>
            <value type="double">100.0</value>
            <value type="double">95.0</value>
            <value type="double">92.5</value>
        </list>
    </property>
</bean>

现在,我们就可以直接以一个数组的方式将属性注入,注意如果是List类型的话,我们也可以使用array数组。同样的,如果是一个Map类型,我们也可以使用entry来注入:

public class Student {
    String name;
    int age;
    Map<String, Double> map;

    public void setMap(Map<String, Double> map) {
        this.map = map;
    }

    public void say(){
        System.out.println("我的成绩:"+ map);
    }
}
<bean name="student" class="com.test.bean.Student">
    <property name="map">
        <map>
            <entry key="语文" value="100.0"/>
            <entry key="数学" value="80.0"/>
            <entry key="英语" value="92.5"/>
        </map>
    </property>
</bean>

我们还可以使用自动装配来实现属性值的注入:

<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student" autowire="byType"/>

自动装配会根据set方法中需要的类型,自动在容器中查找是否存在对应类型或是对应名称以及对应构造方法的Bean,比如我们上面指定的为byType,那么其中的card属性就会被自动注入类型为Card的Bean

我们已经了解了如何使用set方法来创建对象,那么能否不使用默认的无参构造方法,而是指定一个有参构造进行对象的创建呢?

我们可以指定构造方法:

<bean name="student" class="com.test.bean.Student">
        <constructor-arg name="name" value="小明"/>
        <constructor-arg index="1" value="18"/>
    </bean>
public class Student {
    String name;
    int age;

    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    public void say(){
        System.out.println("我是:"+name+"今年"+age+"岁了!");
    }
}

通过手动指定构造方法参数,我们就可以直接告诉容器使用哪一个构造方法来创建对象。


面向切面AOP

AOP思想实际上就是:在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

也就是说,我们可以使用AOP来帮助我们在方法执行前或执行之后,做一些额外的操作,实际上,就是代理!

通过AOP我们可以在保证原有业务不变的情况下,添加额外的动作,比如我们的某些方法执行完成之后,需要打印日志,那么这个时候,我们就可以使用AOP来帮助我们完成,它可以批量地为这些方法添加动作。

可以说,它相当于将我们原有的方法,在不改变源代码的基础上进行了增强处理。

点击查看源网页

相当于我们的整个业务流程,被直接斩断,并在断掉的位置添加了一个额外的操作,再连接起来,也就是在一个切点位置插入内容。

它的原理实际上就是通过动态代理机制实现的,我们在JavaWeb阶段已经给大家讲解过动态代理了。

不过Spring底层并不是使用的JDK提供的动态代理,而是使用的第三方库实现,它能够以父类的形式代理,而不是接口。

使用SpringAOP

Spring是支持AOP编程的框架之一(实际上它整合了AspectJ框架的一部分),要使用AOP我们需要先导入一个依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.13</version>
</dependency>

那么,如何使用AOP呢?

首先我们要明确,要实现AOP操作,我们需要知道这些内容:

  1. 需要切入的类,类的哪个方法需要被切入
  2. 切入之后需要执行什么动作
  3. 是在方法执行前切入还是在方法执行后切入
  4. 如何告诉Spring需要进行切入

那么我们依次来看,首先需要解决的问题是,找到需要切入的类:

public class Student {
    String name;
    int age;

	//分别在test方法执行前后切入
    public int test(String str) {
        System.out.println("我是一个测试方法:"+str);
        return str.length();
    }
}

现在我们希望在test方法执行前后添加我们的额外执行的内容,接着,我们来看看如何为方法执行前和执行后添加切入动作。

比如现在我们想在方法返回之后,再执行我们的动作,首先定义我们要执行的操作:

public class AopTest {

    //执行之后的方法
    public void after(){
        System.out.println("我是执行之后");
    }

    //执行之前的方法
    public void before(){
        System.out.println("我是执行之前");
    }
}

那么,现在如何告诉Spring我们需要在方法执行之前和之后插入其他逻辑呢?

首先我们将要进行AOP操作的类注册为Bean:

<bean name="student" class="com.test.bean.Student"/>
<bean name="aopTest" class="com.test.aop.AopTest"/>

一个是Student类,还有一个就是包含我们要切入方法的AopTest类,注册为Bean后,他们就交给Spring进行管理,这样Spring才能帮助我们完成AOP操作。

接着,我们需要告诉Spring,我们需要添加切入点,首先将顶部修改为,引入aop相关标签:

<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

通过使用aop:config来添加一个新的AOP配置:

<aop:config>
    
</aop:config>

首先第一行,我们需要告诉Spring,我们要切入的是哪一个类的哪个或是哪些方法:

<aop:pointcut id="test" expression="execution(* com.test.bean.Student.test(String))"/>

其中,expression属性的execution填写格式如下:

修饰符 包名.类名.方法名称(方法参数)
  • 修饰符:public、protected、private、包括返回值类型、static等等(使用*代表任意修饰符)
  • 包名:如com.test(代表全部,比如com.代表com包下的全部包)
  • 类名:使用*也可以代表包下的所有类
  • 方法名称:可以使用*代表全部方法
  • 方法参数:填写对应的参数即可,比如(String, String),也可以使用*来代表任意一个参数,使用..代表所有参数。

也可以使用其他属性来进行匹配,比如@annotation可以用于表示标记了哪些注解的方法被切入。

接着,我们需要为此方法添加一个执行前动作和一个执行后动作:

<aop:aspect ref="aopTest">
    <aop:before method="before" pointcut-ref="test"/>
    <aop:after-returning method="after" pointcut-ref="test"/>
</aop:aspect>

这样,我们就完成了全部的配置,现在来实验一下吧:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = context.getBean(Student.class);
    student.test("mllumingfei");
}

我们发现,方法执行前后,分别调用了我们对应的方法。但是仅仅这样还是不能满足一些需求,在某些情况下,我们可以需求方法执行的一些参数,比如方法执行之后返回了什么,或是方法开始之前传入了什么参数等等。

这个时候,我们可以为我们切入的方法添加一个参数,通过此参数就可以快速获取切点位置的一些信息:

//执行之前的方法
public void before(JoinPoint point){
    System.out.println("我是执行之前");
    System.out.println(point.getTarget());  //获取执行方法的对象
    System.out.println(Arrays.toString(point.getArgs()));  //获取传入方法的实参
}

通过添加JoinPoint作为形参,Spring会自动给我们一个实现类对象,这样我们就能获取方法的一些信息了。

最后我们再来看环绕方法,环绕方法相当于完全代理了此方法,它完全将此方法包含在中间,需要我们手动调用才可以执行此方法,并且我们可以直接获取更多的参数:

public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("方法开始之前");
    Object value = joinPoint.proceed();
    System.out.println("方法执行完成,结果为:"+value);
    return value;
}

注意,如果代理方法存在返回值,那么环绕方法也需要有一个返回值,通过proceed方法来执行代理的方法,也可以修改参数之后调用proceed(Object[]),使用我们给定的参数再去执行:

public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("方法开始之前");
    String arg = joinPoint.getArgs()[0] + "伞兵一号lbw";
    Object value = joinPoint.proceed(new Object[]{arg});
    System.out.println("方法执行完成,结果为:"+value);
    return value;
}

使用接口实现AOP

前面我们介绍了如何使用xml配置一个AOP操作,我们来看看如何使用Advice实现AOP。

它与我们之前学习的动态代理更接近一些,比如在方法开始执行之前或是执行之后会去调用我们实现的接口,

首先我们需要将一个类实现Advice接口,只有实现此接口,才可以被通知,比如我们这里使用MethodBeforeAdvice表示是一个在方法执行之前的动作:

public class AopTest implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("通过Advice实现AOP");
    }
}

我们发现,方法中包括了很多的参数,其中args代表的是方法执行前得到的实参列表,还有target表示执行此方法的实例对象。

运行之后,效果和之前是一样的,但是在这里我们就可以快速获取到更多信息。

<aop:config>
    <aop:pointcut id="stu" expression="execution(* com.test.bean.Student.say(String))"/>
    <aop:advisor advice-ref="before" pointcut-ref="stu"/>
</aop:config>

除了此接口以外,还有其他的接口,比如AfterReturningAdvice就需要实现一个方法执行之后的操作:

public class AopTest implements MethodBeforeAdvice, AfterReturningAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("我是方法执行之前!");
    }

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("我是方法执行之后!");
    }
}

其实,我们之前学习的操作正好对应了AOP 领域中的特性术语

  • 通知(Advice): AOP 框架中的增强处理,通知描述了切面何时执行以及如何执行增强处理,也就是我们上面编写的方法实现。
  • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出,实际上就是我们在方法执行前或是执行后需要做的内容。
  • 切点(PointCut): 可以插入增强处理的连接点,可以是方法执行之前也可以方法执行之后,还可以是抛出异常之类的。
  • 切面(Aspect): 切面是通知和切点的结合,我们之前在xml中定义的就是切面,包括很多信息。
  • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,我们之前都是在将我们的增强处理添加到目标对象。

使用注解开发

前面我们已经了解了IoC容器和AOP实现,但是我们发现,要使用这些功能,我们就不得不编写大量的配置,这是非常浪费时间和精力的,并且我们还只是演示了几个小的例子,如果是像之前一样去编写一个完整的Web应用程序,那么产生的配置可能会非常多。能否有一种更加高效的方法能够省去配置呢?

当然还是注解了。

注解实现配置文件

那么,现在既然不使用XML文件了,那通过注解的方式就只能以实体类的形式进行配置了,我们在要作为配置的类上添加@Configuration注解,我们这里创建一个新的类MainConfiguration

@Configuration
public class MainConfiguration {
    //没有配置任何Bean
}

你可以直接把它等价于:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
		<!-- 没有配置任何Bean -->
</beans>

那么我们来看看,如何配置Bean,之前我们是直接在配置文件中编写Bean的一些信息,现在在配置类中,我们只需要编写一个方法,并返回我们要创建的Bean的对象即可,并在其上方添加@Bean注解:

@Bean
public Card card(){
    return new Card();
}

这样,等价于:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
		<bean class="com.test.bean.Card"></bean>
</beans>

我们还可以继续添加@Scope注解来指定作用域,这里我们就用原型模式:

@Bean
@Scope("prototype")
public Card card(){
    return new Card();
}

采用这种方式,我们就可以更加方便地控制一个Bean对象的创建过程,现在相当于这个对象时由我们创建好了再交给Spring进行后续处理,我们可以在对象创建时做很多额外的操作,包括一些属性值的配置等。

既然现在我们已经创建好了配置类,接着我们就可以在主方法中加载此配置类,并创建一个基于配置类的容器:

public class Main {
    public static void main(String[] args) {
      	//使用AnnotationConfigApplicationContext来实现注解配置
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class); //这里需要告诉Spring哪个类作为配置类
        Card card = context.getBean(Card.class);  //容器用法和之前一样
        System.out.println(card);
    }
}

在配置的过程中,我们可以点击IDEA底部的Spring标签,打开后可以对当前向容器中注册的Bean进行集中查看,并且会标注Bean之间的依赖关系,我们可以发现,Bean的默认名称实际上就是首字母小写的方法名称,我们也可以手动指定:

@Bean("ml")
@Scope("prototype")
public Card card(){
    return new Card();
}

除了像原来一样在配置文件中创建Bean以外,我们还可以直接在类上添加@Component注解来将一个类进行注册(现在最常用的方式)

不过要实现这样的方式,我们需要添加一个自动扫描,来告诉Spring需要在哪些包中查找我们提供@Component声明的Bean。

只需要在配置类上添加一个@ComponentScan注解即可,如果要添加多个包进行扫描,可以使用@ComponentScans来批量添加。这里我们演示将bean包下的所有类进行扫描:

@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {

}

现在删除类中的Bean定义,我们在Student类的上面添加@Component注解,来表示此类型需要作为Bean交给容器进行管理:

@Component
@Scope("prototype")
public class Student {
    String name;
    int age;
    Card card;
}

同样的,在类上也可以直接添加@Scope来限定作用域。

效果和刚刚实际上是相同的,我们可以来测试一下:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);
        System.out.println(context.getBean(Student.class));
    }
}

我们可以看到IDEA的Spring板块中也显示了我们刚刚通过直接在类上添加@Component声明的Bean。

@Component同样效果的还有@Controller@Service@Repository,但是现在暂时不提,讲到SpringMVC时再来探讨。

现在我们就有两种方式注册一个Bean了,那么如何实现像之前一样的自动注入呢,比如我们将Card也注册为Bean,我们希望Spring自动将其注入到Student的属性中:

@Component
public class Student {
    String name;
    int sid;
    Card card;
}

因此,我们可以将此类型,也通过这种方式注册为一个Bean:

@Component
@Scope("prototype")
public class Card {
}

现在,我们在需要注入的位置,添加一个@Resource注解来实现自动装配:

@Component
public class Student {
    String name;
    int sid;
    
    @Resource
    Card card;
}

这样的好处是,我们完全不需要创建任何的set方法,只需要添加这样的一个注解就可以了,Spring会跟之前配置文件的自动注入一样,在整个容器中进行查找,并将对应的Bean实例对象注入到此属性中,当然,如果还是需要通过set方法来注入,可以将注解添加到方法上:

@Component
public class Student {
    String name;
    int sid;
    Card card;

    @Resource
    public void setCard(Card card) {
        System.out.println("通过方法");
        this.card = card;
    }
}

除了使用@Resource以外,我们还可以使用@Autowired(IDEA不推荐将其使用在字段上,会出现黄标,但是可以放在方法或是构造方法上),它们的效果是一样的,但是它们存在区别,虽然它们都是自动装配:

  • @Resource默认ByName如果找不到则ByType,可以添加到set方法、字段上。
  • @Autowired默认是byType,可以添加在构造方法、set方法、字段、方法参数上。

并且@Autowired可以配合@Qualifier使用,来指定一个名称的Bean进行注入:

@Autowired
@Qualifier("sxc")
public void setCard(Card card) {
    System.out.println("通过方法");
    this.card = card;
}

如果Bean是在配置文件中进行定义的,我们还可以在方法的参数中使用@Autowired来进行自动注入:

@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {

    @Bean
    public Student student(@Autowired Card card){
        Student student = new Student();
        student.setCard(card);
        return student;
    }
}

我们还可以通过@PostConstruct注解来添加构造后执行的方法,它等价于之前讲解的init-method

@PostConstruct
public void init(){
    System.out.println("我是初始化方法!1");
}

注意它们的顺序:Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct

同样的,如果需要销毁方法,也可以使用@PreDestroy注解,这里就不做演示了。

这样,两种通过注解进行Bean声明的方式就讲解完毕了,那么什么时候该用什么方式去声明呢?

  • 如果要注册为Bean的类是由其他框架提供,我们无法修改其源代码,那么我们就使用第一种方式进行配置。
  • 如果要注册为Bean的类是我们自己编写的,我们就可以直接在类上添加注解,并在配置中添加扫描。

注解实现AOP操作

了解了如何使用注解注册Bean之后,我们接着来看如何通过注解实现AOP操作,首先我们需要在主类添加@EnableAspectJAutoProxy注解,开启AOP注解支持:

@EnableAspectJAutoProxy
@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {
}

接着我们只需在定义AOP增强操作的类上添加@Aspect注解和@Component将其注册为Bean即可,就像我们之前在配置文件中也要将其注册为Bean:

@Component
@Aspect
public class AopTest {

}

接着,我们直接在里面编写方法,并将此方法添加到一个切点中,比如我们希望在Student的test方法执行之前执行我们的方法:

public int test(String str){
    System.out.println("我被调用了:"+str);
    return str.length();
}

只需要添加@Before注解即可:

@Before("execution(* com.test.bean.Student.test(..))")
public void before(){
    System.out.println("我是之前执行的内容!");
}

同样的,我们可以为其添加JoinPoint参数来获取切入点信息:

@Before("execution(* com.test.bean.Student.test(..))")
public void before(JoinPoint point){
    System.out.println("参数列表:"+ Arrays.toString(point.getArgs()));
    System.out.println("我是之前执行的内容!");
}

我们也可以使用@AfterReturning注解来指定方法返回后的操作:

@AfterReturning(value = "execution(* com.test.bean.Student.test(..))", returning = "returnVal")
public void after(Object returnVal){
    System.out.println("方法已返回,结果为:"+returnVal);
}

我们还可以指定returning属性,并将其作为方法某个参数的实参。同样的,环绕也可以直接通过注解声明:

@Around("execution(* com.test.bean.Student.test(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
    System.out.println("方法执行之前!");
    Object val = point.proceed();
    System.out.println("方法执行之后!");
    return val;
}

其他注解配置

配置文件可能不止一个,我们有可能会根据模块划分,定义多个配置文件,这个时候,可能会出现很多个配置类,如果我们需要@Import注解来快速将某个类加入到容器中,比如我们现在创建一个新的配置文件,并将数据库Bean也搬过去:

public class Test2Configuration {
    @Bean
    public Connection getConnection() throws SQLException {
        System.out.println("创建新的连接!");
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/study",
                "root",
                "root");
    }
}
@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.test")
@Import(Test2Configuration.class)
public class TestConfiguration {

    @Resource
    Connection connection;

    @PostConstruct
    public void init(){
        System.out.println(connection);
    }
}

注意另一个配置类并没有添加任何注解,实际上,相当于导入的类被强制注册为了一个Bean,到现在,我们一共了解了三种注册为Bean的方式,利用这种特性,我们还可以将其他的类型也强制注册为Bean:

@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.test")
@Import({Test2Configuration.class, Date.class})
public class TestConfiguration {

    @Resource
    Connection connection;
    @Resource
    Date date;

    @PostConstruct
    public void init(){
        System.out.println(date+" -> "+connection);
    }
}

可以看到,日期直接作为一个Bean放入到IoC容器中了,并且时间永远都是被new的那个时间,也就是同一个对象(因为默认是单例模式)。

通过@Import方式最主要为了实现的目标并不是创建Bean,而是为了方便一些框架的Registrar进行Bean定义,在讲解到Spring原理时,我们再来详细讨论,目前只做了解即可。

到这里,关于Spring框架的大致内容就聊得差不多了,其余的内容,我们会在后面继续讲解。

posted @ 2022-03-05 20:07  Ricardo_ML  阅读(60)  评论(0编辑  收藏  举报