Spring入门

https://blog.csdn.net/ncepu_Chen/article/details/91903396

解耦

从解耦开始聊,耦合就是程序的依赖关系。为什么需要解耦,因为实际中需求变化多端,你如果写死了你代码,将来要改会非常非常恶心。但是彻底的解除耦合是不可能的,因为就像这个社会一样,没有人能够不和其他人发生关系,程序中的类和方法也必然会和别的类、方法发生关系。

比如加载数据库驱动的代码:

1
2
3
4
5
// 最常用的
Class.forName("com.....");

// 其实也可以这么写
DriverManager.registerManager(new xxx.xxx.xxx.Driver());

我们为什么一般都是第一种而不是第二种呢,它们本质上是一样的,都是加载了jar包中的类,然后注册那个驱动。

第一,如果你的包不存在,那么反射的写法在编译的时候是不会报错的,但是第二种会报错。所以第一个原则就是:编译期间不去依赖,到运行的时候才去依赖,也就是少用new,而是使用反射来创建对象。

第二,将来你要是想要修改数据库驱动,那么你需要直接修改源代码,所以第二个原则就是使用配置文件

实际使用中,比如最常见的三层架构,表现层需要new一个业务层的对象来进行处理,业务层也需要new一个持久层的对象来进行数据库的处理,这就造成了耦合。所以由此获得灵感,我们可以使用工厂模式,配合上反射+使用配置文件的方式来完成解耦。

工厂模式还有一个小小的缺点,就是每次创建对象都是不同的,所以我们需要使用单例模式来让其在内存里只有一个对象,当然也可以用一个map来进行操作,在类加载的时候初始化这个map,之后要用拿出来就行了。

IOC

平时我们创建对象,都是new一个出来。相当于你需要直接和目标打交道;当有了工厂之后,你是直接去找工厂索要对象,工厂本身通过一些方式来控制对象(如上面的map)。所以,这就叫Inversion of Control,IOC控制反转。原来完全由你来主导的,变为了你传递一个字符串给工厂,由工厂来帮你创建对象,你再也无法决定来new谁了。

1
2
3
4
5
6
7
8
9
10
// 获取核心容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");

// 根据核心容器就可以获取对象了,分别是强转和直接传入类对象来构建
IAccountService as = (IAccountService) ac.getBean("accountService");
IAccountDao ad = ac.getBean("accountDao", AccountDaoImpl.class);

// 获取成功
System.out.println(as);
System.out.println(ad);

这里的核心容器即为ApplicationContext,它是一个接口,底下有三个实现类,分别是:

  • ClassPathXmlApplicationContext,加载类路径下面的配置文件。
  • FileSystemXmlApplicationContext,加载任意路径下的配置文件,但是不推荐使用
  • AnnotationConfigApplicationContext,加载注解的配置文件。

这个核心容器采用的思想是立即加载,即只要读取完配置文件,就会通过反射创建好对象放入到容器里。还有另外一个核心容器叫BeanFactory,只有当需要对象的时候才会new出来。

1
2
3
4
5
6
7
8
9
10
11
// 获取核心容器
Resource resource = new ClassPathResource("bean.xml");
BeanFactory factory = new XmlBeanFactory(resource);

// 根据核心容器就可以获取对象了,分别是强转和直接传入类对象来构建
IAccountService as = (IAccountService) factory.getBean("accountService");
IAccountDao ad = factory.getBean("accountDao", AccountDaoImpl.class);

// 获取成功
System.out.println(as);
System.out.println(ad);

由于service和dao本质上是单例,所以比较推荐第一种,即只要读取完配置文件就创建。

bean

首先说明,之前我在大学上课的时候,老师讲的是,javabean就是一个拥有getter、setter和类变量的类。但是其实不是的,bean表示的是可以重复使用的对象,当然之前的实体类要说是bean也是对的,只是实体类是bean的一种。

创建bean的三种方式

  1. 直接使用类的默认构造器来构造对象,这样只需要一个bean标签,里面写好id和全限定类名即可。如果没有默认构造函数,就会失败!
1
2
3
<bean id="accountDao" class="cn.chenlangping.dao.impl.AccountDaoImpl">
<!-- collaborators and configuration for this bean go here -->
</bean>
  1. 某个工厂类中的某个方法返回值是我们需要的对象。我们首先用一个bean对象指定工厂,然后再写一个bean,在标签里面写入对应的工厂名和工厂方法即可。
1
2
3
4
5
6
// 工厂类
public class MyFactory {
public AccountServiceImpl getAccountService() {
return new AccountServiceImpl();
}
}
1
2
<bean id="myFactory" class="cn.chenlangping.factory.MyFactory"></bean>
<bean id="accountService" factory-bean="myFactory" factory-method="getAccountService"></bean>
  1. 工厂类中的静态方法会返回一个我们需要的对象。
1
<bean id="accountService" class="cn.chenlangping.factory.MyFactory" factory-method="getAccountService"></bean>

2和3的区别也很好理解,因为2中的不是静态方法,所以需要通过反射来构建一个工厂,而3则不需要,直接就可以通过工厂来获取。

bean的作用范围

默认情况下,bean是一个单例。可以通过标签下的scope标签来进行调整。

  • singleton:默认值,单例
  • prototype:多例的。
  • request:作用于web应用的请求范围。
  • session:作用于Web应用的会话范围。
  • global-session:作用于集群的环境的范围。不是集群就是session。

bean的生命周期

不同类型的对象,生命周期不同。

  • 单例对象:随容器一起,容器创建它创建;容器存在它活着;容器销毁它死亡。
  • 多例对象:使用对象的时候容器为我们创建;对象只要使用的时候就一直活着;通过垃圾回收机制来回收。

依赖注入

通过控制反转,我们把创建对象的任务托管给了spring框架,但是代码中不可能消除所有依赖,例如:业务层仍然会调用持久层的方法,因此业务层类中应包含持久化层的实现类对象。
我们使用框架,通过配置的方式,将持久层对象传入业务层,而不是直接在代码中new某个具体的持久化层实现类,这种方式称为依赖注入

注入方式

使用构造函数注入

由于构造函数大部分都是会带参数的,这个时候我们就需要向spring框架提供信息来让其能够创建对象。<constructor-arg>标签就是用来做这个事情的,它有以下五个属性(我们需要确定参数的位置和参数的类型):

  1. index:指定参数在构造函数参数列表的索引位置,从0开始,用它可以唯一标识。
  2. type:指定数据的数据类型。但是很多情况下只靠参数的类型其实是无法确定是哪个参数的,所以一般要配合别的使用。
  3. name:指定参数的名字来找参数。一般都用它。
  4. value:参数的值,比如如果你要传入一个int,你需要写value="18",它是为了给基本类型和String类型赋值的。
  5. ref:对于不是基本类型的,比如date,我们如果要取它作为参数,那么需要首先写一个标签bean,然后定义好全限定类名和id,最后回到别的类里就可以用id使用了。

它的弊端就是,你必须提供构造函数的所有参数,一个都不能少。

使用setXxx函数注入

首先你的类必须为各种属性设立了各种setter方法,标签只剩下namevalueref三个了,name属性中写入你setXxx方法后面去掉set并且将首字母变为小写的值。

这个的好处就是解决了上面使用构造函数注入的缺点。但是缺点是无法保证某个属性必须有值。

集合类型注入

还有一些ListSet等集合类型的注入:

只有键的结构:

  • 数组字段: <array>标签表示集合,<value>标签表示集合内的成员。
  • List字段:<list>标签表示集合,<value>标签表示集合内的成员。
  • Set字段: <set>标签表示集合,<value>标签表示集合内的成员。
  • 其中<array><list><set>标签之间可以互相替换使用。

键值对的结构:

  • Map字段:<map>标签表示集合,<entry>标签表示集合内的键值对,其key属性表示键,value属性表示值。
  • Properties字段:<props>标签表示集合,<prop>标签表示键值对,其key属性表示键,标签内的内容表示值。
  • 其中<map><props>标签之间,<entry><prop>标签之间可以互相替换使用。

使用注解

之前一直使用配置文件,这里我们使用注解来简化操作。

创建对象

这些注解的作用相当于bean.xml中的<bean>标签。如果要使用注解,记得在bean.xml中加入扫描包的代码。

  • @Component:把当前类对象存入spring容器中,有一个属性value
    • value: 用于指定当前类的id。如果不写,则默认值是当前类名首字母改小写
  • @Controller:将当前表现层对象存入spring容器中
  • @Service:将当前业务层对象存入spring容器中
  • @Repository:将当前持久层对象存入spring容器中

上面这四个注解,除了名字以外其实完全一致,取不同的名字就是为了区别不同的作用层。也就是我们之后如果想创建一个对象并将其加入到容器中,那么只需要在定义类上面加个注解即可。

注入数据

这里我只想写一个:@Resource,直接按照bean的id注入。

改变作用域

@Scope,其值和之前作用域的五个值一样。

生命周期

  1. @PostConstruct:用于指定初始化方法
  2. @PreDestroy:用于指定销毁方法,别忘了如果是多例的话,是由虚拟机来决定是什么时候摧毁的。

其它部分

那么还有一些问题,有些类我们无法为其加入注解,那应该怎么办呢?比如jar包中的类,很显然我们无法修改其代码,自然也不能为其加入注解。还有一个问题是,上面也提到了,还是需要在bean.xml中加入扫描包的代码,虽然已经很简化了,那有没有办法甚至连bean.xml都不需要呢?

首先来解决第二个问题,我们希望用纯的注解来解决问题,即完完全全删除bean.xml。创建一个类,然后再这个类上面加上@Configuration注解,那么这个类就成为了一个配置类,之后spring会来找它创建容器。然后再这个类上面再加上@ComponentScan("包地址"),这样就完成了基础配置,就不需要bean.xml中扫描包的代码了。

1
2
3
4
@Configuration
@ComponentScan("cn.chenlangping")
public class SpringConfiguration {
}

接下来解决第一个问题,也就是我们需要为那些我们不能加注解的类来生成bean并且放入容器中,当然我们可以通过在之前那个配置类中创建方法,然后调用方法来创建新的对象来达成,但是这么做并不会将新创建的对象放入到容器中,我们需要在方法上加上@Bean这个注解,这样被它注解的方法的返回值就能够成功被放入容器中了。

和Junit整合

由于Junit并不知晓spring框架,所以我们需要加入spring-test来完成单元测试功能。

AOP

Aspect Oriented Programming,面向切面编程,通过使用动态代理的技术,来降低程序的耦合。所以有必要了解下动态代理技术。

动态代理技术

动态代理技术作为设计模式中的一种,它的主要目的是为了给函数增强它的功能,比如某个函数在写的时候没有加上日志的功能,这个时候就需要在运行的时候为它加上这个功能。目前主要的动态代理有两种,一种是JDK自带支持的,另外一种是cglib提供的。

JDK自带的

原理:客户端会调用接口的方法。运行时根据目标类动态创建代理类,代理类和目标类实现相同的接口。调用方调用代理类,代理类反射调用目标类。

优点:JDK自带,完全不需要导包。

缺点:被代理的类必须实现一个接口才可以。

示例代码:

1
2
3
4
// 一个普通的接口,里面就一个打印方法的定义
public interface IPrinter {
void print();
}
1
2
3
4
5
6
7
// 实现类
public class Printer implements IPrinter {
@Override
public void print() {
System.out.println("print");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 动态代理它
public static void main(String[] args) {
IPrinter iPrinter = new Printer();
iPrinter.print();

IPrinter iPrinter2 = (IPrinter) Proxy.newProxyInstance(Printer.class.getClassLoader(), Printer.class.getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
Object returnValue = method.invoke(iPrinter, args);
System.out.println("after");
return returnValue;
}
});
iPrinter2.print();
}

简单的来说就是通过Proxy的静态方法来创建一个对象,最后调用这个对象的方法即可。比较烦的是如何创建一个代理对象,首先传入被代理对象的类的类加载器、它们共通的接口以及一个InvocationHandler实例(上面使用了匿名内部类,实际上还可以用lambda表达式),分别有三个参数,是代理的对象、执行的方法和方法所带的函数。

cglib提供的

由于JDK自带的代理需要某个类实现了接口才可以动态代理,就有了这个第三方的jar实现。

原理:运行时根据目标类动态创建代理类,代理类是目标类的子类。然后通过这个子类来实现父类中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Printer iPrinter = new Printer();
iPrinter.print();

Printer printer = (Printer) Enhancer.create(iPrinter.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before");
Object returnValue = method.invoke(iPrinter, objects);
System.out.println("after");
return returnValue;
}
});
printer.print();
}

实现代码几乎和上面的JDK的一样,只是少了一个接口,但是因为它是通过子类实现的,所以实现类中独有的那些方法也可以被加强,这是接口所做不到的。

优点:只要一个类它不是final的就可以被加强。

Spring中的AOP

作用:之前也讲到了,在实际中遇到重复的代码,我们的做法是抽取出来并且封装成函数来调用,但是这么做会造成方法之间的依赖,于是就有了Spring的AOP帮我们解决这个问题。注意,需要依赖aspectjweaver,请不要忘记添加这个依赖。

假设需要为某个类中的某个方法进行增强,比如为这个方法加上一个日志记录的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义接口
public interface INormal {
void test1();

void test2(int i);

int test3();
}

// 实现类
public class NormalImpl implements INormal {
@Override
public void test1() {
System.out.println("test1");
}

@Override
public void test2(int i) {
System.out.println("test2");
}

@Override
public int test3() {
System.out.println("test3");
return 0;
}
}

现在需要为这里的三个方法增加一个日志记录的功能,所以首先需要去定义一个日志类,并在其内部完成功能:

1
2
3
4
5
public class Logger {
public void printLog(){
System.out.println("打印日志");
}
}

最后就是配置Spring,让它把我们定义的打印日志的功能,加入到上面三个方法中。

通过XML的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置两个bean对象放入容器中 -->
<bean id="normal" class="cn.chenlangping.aop.NormalImpl"></bean>
<bean id="logger" class="cn.chenlangping.aop.Logger"></bean>

<!-- 配置AOP -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<aop:before method="printLog" pointcut="execution(public void cn.chenlangping.aop.NormalImpl.test1())"/>
<aop:after method="printLog" pointcut="execution(public void cn.chenlangping.aop.NormalImpl.test2(int))"/>
</aop:aspect>
</aop:config>
</beans>

真心觉得特别简单,放入IOC容器这个是必须的,否则spring怎么帮你创建对象并管理呢?然后只要想好,哪个类的哪个函数,需要和另外一个类中的一个函数发生关系,是怎么样的关系即可。

通过注解的方式

首先是需要实现类成为一个组件(相当于声明为bean对象,此处略),然后需要修改下Logger类,为他加上几个注解即可:

1
2
3
4
5
6
7
8
@Component
@Aspect
public class Logger {
@Before("execution(public void cn.chenlangping.aop.NormalImpl.test1())")
public void printLog(){
System.out.println("打印日志");
}
}

xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="cn.chenlangping.aop"/>
<aop:aspectj-autoproxy/>

</beans>

最后再结合xml文件,只需要简单的定义扫描的包和开启AOP支持即可。虽然可以用类来使用纯注解,但是我个人认为还是xml结合注解会方便的多。

Spring JdbcTemplate

对JDBC进行了一些简单的封装。