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。
在我们之前的图书管理系统Web应用程序中,我们发现,整个程序其实是依靠各个部分相互协作,共同完成一个操作,比如要展示借阅信息列表,那么首先需要使用Servlet进行请求和响应的数据处理,然后请求的数据全部交给对应的Service(业务层)来处理,当Service发现要从数据库中获取数据时,再向对应的Mapper发起请求。
它们之间就像连接在一起的齿轮,谁也离不开谁:
就像一个团队,每个人的分工都很明确,流水线上的一套操作必须环环相扣,这是一种高度耦合的体系。
虽然这样的体系逻辑非常清晰,整个流程也能够让人快速了解,但是这样存在一个很严重的问题,我们现在的时代实际上是一个软件项目高速迭代的时代,我们发现很多App三天两头隔三差五地就更新,而且是什么功能当下最火,就马不停蹄地进行跟进开发,因此,就很容易出现,之前写好的代码,实现的功能,需要全部推翻,改成新的功能,那么我们就不得不去修改某些流水线上的模块,但是这样一修改,会直接导致整个流水线的引用关系大面积更新。
就像我不想用这个Service实现类了,我想使用其他的实现类用不同的逻辑做这些功能,那么这个时候,我们只能每个类都去挨个进行修改,当项目特别庞大时,光是改个类名就够你改一天。
因此,高耦合度带来的缺点是很明显的,也是现代软件开发中很致命的问题。如果要改善这种情况,我们只能将各个模块进行解耦,让各个模块之间的依赖性不再那么地强。也就是说,Service的实现类,不再由我们决定,而是让程序自己决定,所有的实现类对象,全部交给程序来管理,所有对象之间的关系,也由程序来动态决定,这样就引入了IoC理论。
IOC是Inversion of Control的缩写,翻译为:“控制反转”,把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。
我们可以将对象交给IoC容器进行管理,比如当我们需要一个接口的实现时,由它根据配置文件来决定到底给我们哪一个实现类,这样,我们就可以不用再关心我们要去使用哪一个实现类了,我们只需要关心,给到我的一定是一个可以正常使用的实现类,能用就完事了,反正接口定义了啥,我只管调,这样,我们就可以放心地让一个人去写视图层的代码,一个人去写业务层的代码,开发效率那是高的一匹啊。
高内聚,低耦合,是现代软件的开发的设计目标,而Spring框架就给我们提供了这样的一个IoC容器进行对象的管理。
首先一定要明确,使用Spring首要目的是为了使得软件项目进行解耦,而不是为了去简化代码!
Spring并不是一个独立的框架,它实际上包含了很多的模块而我们首先要去学习的就是Core Container,也就是核心容器模块。
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
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的详细配置:
<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属性。
现在我们已经了解了如何注册和使用一个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来帮助我们完成,它可以批量地为这些方法添加动作。可以说,它相当于将我们原有的方法,在不改变源代码的基础上进行了增强处理。
相当于我们的整个业务流程,被直接斩断,并在断掉的位置添加了一个额外的操作,再连接起来,也就是在一个切点位置插入内容。它的原理实际上就是通过动态代理机制实现的,我们在JavaWeb阶段已经给大家讲解过动态代理了。不过Spring底层并不是使用的JDK提供的动态代理,而是使用的第三方库实现,它能够以父类的形式代理,而不是接口。
Spring是支持AOP编程的框架之一(实际上它整合了AspectJ框架的一部分),要使用AOP我们需要先导入一个依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.13</version>
</dependency>
那么,如何使用AOP呢?首先我们要明确,要实现AOP操作,我们需要知道这些内容:
那么我们依次来看,首先需要解决的问题是,找到需要切入的类:
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("lbwnb");
}
我们发现,方法执行前后,分别调用了我们对应的方法。但是仅仅这样还是不能满足一些需求,在某些情况下,我们可以需求方法执行的一些参数,比如方法执行之后返回了什么,或是方法开始之前传入了什么参数等等。
这个时候,我们可以为我们切入的方法添加一个参数,通过此参数就可以快速获取切点位置的一些信息:
//执行之前的方法
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] + "伞兵一号";
Object value = joinPoint.proceed(new Object[]{arg});
System.out.println("方法执行完成,结果为:"+value);
return value;
}
前面我们介绍了如何使用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): 将增强处理添加到目标对象中,并创建一个被增强的对象,我们之前都是在将我们的增强处理添加到目标对象,也就是织入(这名字挺有文艺范的)