解耦
从解耦开始聊,耦合就是程序的依赖关系。为什么需要解耦,因为实际中需求变化多端,你如果写死了你代码,将来要改会非常非常恶心。但是彻底的解除耦合是不可能的,因为就像这个社会一样,没有人能够不和其他人发生关系,程序中的类和方法也必然会和别的类、方法发生关系。
比如加载数据库驱动的代码:
1 | // 最常用的 |
我们为什么一般都是第一种而不是第二种呢,它们本质上是一样的,都是加载了jar包中的类,然后注册那个驱动。
第一,如果你的包不存在,那么反射的写法在编译的时候是不会报错的,但是第二种会报错。所以第一个原则就是:编译期间不去依赖,到运行的时候才去依赖,也就是少用new,而是使用反射来创建对象。
第二,将来你要是想要修改数据库驱动,那么你需要直接修改源代码,所以第二个原则就是使用配置文件。
实际使用中,比如最常见的三层架构,表现层需要new一个业务层的对象来进行处理,业务层也需要new一个持久层的对象来进行数据库的处理,这就造成了耦合。所以由此获得灵感,我们可以使用工厂模式,配合上反射+使用配置文件的方式来完成解耦。
工厂模式还有一个小小的缺点,就是每次创建对象都是不同的,所以我们需要使用单例模式来让其在内存里只有一个对象,当然也可以用一个map来进行操作,在类加载的时候初始化这个map,之后要用拿出来就行了。
IOC
平时我们创建对象,都是new一个出来。相当于你需要直接和目标打交道;当有了工厂之后,你是直接去找工厂索要对象,工厂本身通过一些方式来控制对象(如上面的map)。所以,这就叫Inversion of Control,IOC控制反转。原来完全由你来主导的,变为了你传递一个字符串给工厂,由工厂来帮你创建对象,你再也无法决定来new谁了。
1 | // 获取核心容器 |
这里的核心容器即为ApplicationContext,它是一个接口,底下有三个实现类,分别是:
- ClassPathXmlApplicationContext,加载类路径下面的配置文件。
- FileSystemXmlApplicationContext,加载任意路径下的配置文件,但是不推荐使用
- AnnotationConfigApplicationContext,加载注解的配置文件。
这个核心容器采用的思想是立即加载,即只要读取完配置文件,就会通过反射创建好对象放入到容器里。还有另外一个核心容器叫BeanFactory,只有当需要对象的时候才会new出来。
1 | // 获取核心容器 |
由于service和dao本质上是单例,所以比较推荐第一种,即只要读取完配置文件就创建。
bean
首先说明,之前我在大学上课的时候,老师讲的是,javabean就是一个拥有getter、setter和类变量的类。但是其实不是的,bean表示的是可以重复使用的对象,当然之前的实体类要说是bean也是对的,只是实体类是bean的一种。
创建bean的三种方式
- 直接使用类的默认构造器来构造对象,这样只需要一个bean标签,里面写好id和全限定类名即可。如果没有默认构造函数,就会失败!
1 | <bean id="accountDao" class="cn.chenlangping.dao.impl.AccountDaoImpl"> |
- 某个工厂类中的某个方法返回值是我们需要的对象。我们首先用一个bean对象指定工厂,然后再写一个bean,在标签里面写入对应的工厂名和工厂方法即可。
1 | // 工厂类 |
1 | <bean id="myFactory" class="cn.chenlangping.factory.MyFactory"></bean> |
- 工厂类中的静态方法会返回一个我们需要的对象。
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>标签就是用来做这个事情的,它有以下五个属性(我们需要确定参数的位置和参数的类型):
index:指定参数在构造函数参数列表的索引位置,从0开始,用它可以唯一标识。type:指定数据的数据类型。但是很多情况下只靠参数的类型其实是无法确定是哪个参数的,所以一般要配合别的使用。name:指定参数的名字来找参数。一般都用它。value:参数的值,比如如果你要传入一个int,你需要写value="18",它是为了给基本类型和String类型赋值的。ref:对于不是基本类型的,比如date,我们如果要取它作为参数,那么需要首先写一个标签bean,然后定义好全限定类名和id,最后回到别的类里就可以用id使用了。
它的弊端就是,你必须提供构造函数的所有参数,一个都不能少。
使用setXxx函数注入
首先你的类必须为各种属性设立了各种setter方法,标签只剩下name、 value和ref三个了,name属性中写入你setXxx方法后面去掉set并且将首字母变为小写的值。
这个的好处就是解决了上面使用构造函数注入的缺点。但是缺点是无法保证某个属性必须有值。
集合类型注入
还有一些List、Set等集合类型的注入:
只有键的结构:
- 数组字段:
<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,其值和之前作用域的五个值一样。
生命周期
@PostConstruct:用于指定初始化方法@PreDestroy:用于指定销毁方法,别忘了如果是多例的话,是由虚拟机来决定是什么时候摧毁的。
其它部分
那么还有一些问题,有些类我们无法为其加入注解,那应该怎么办呢?比如jar包中的类,很显然我们无法修改其代码,自然也不能为其加入注解。还有一个问题是,上面也提到了,还是需要在bean.xml中加入扫描包的代码,虽然已经很简化了,那有没有办法甚至连bean.xml都不需要呢?
首先来解决第二个问题,我们希望用纯的注解来解决问题,即完完全全删除bean.xml。创建一个类,然后再这个类上面加上@Configuration注解,那么这个类就成为了一个配置类,之后spring会来找它创建容器。然后再这个类上面再加上@ComponentScan("包地址"),这样就完成了基础配置,就不需要bean.xml中扫描包的代码了。
1 |
|
接下来解决第一个问题,也就是我们需要为那些我们不能加注解的类来生成bean并且放入容器中,当然我们可以通过在之前那个配置类中创建方法,然后调用方法来创建新的对象来达成,但是这么做并不会将新创建的对象放入到容器中,我们需要在方法上加上@Bean这个注解,这样被它注解的方法的返回值就能够成功被放入容器中了。
和Junit整合
由于Junit并不知晓spring框架,所以我们需要加入spring-test来完成单元测试功能。
AOP
Aspect Oriented Programming,面向切面编程,通过使用动态代理的技术,来降低程序的耦合。所以有必要了解下动态代理技术。
动态代理技术
动态代理技术作为设计模式中的一种,它的主要目的是为了给函数增强它的功能,比如某个函数在写的时候没有加上日志的功能,这个时候就需要在运行的时候为它加上这个功能。目前主要的动态代理有两种,一种是JDK自带支持的,另外一种是cglib提供的。
JDK自带的
原理:客户端会调用接口的方法。运行时根据目标类动态创建代理类,代理类和目标类实现相同的接口。调用方调用代理类,代理类反射调用目标类。
优点:JDK自带,完全不需要导包。
缺点:被代理的类必须实现一个接口才可以。
示例代码:
1 | // 一个普通的接口,里面就一个打印方法的定义 |
1 | // 实现类 |
1 | // 动态代理它 |
简单的来说就是通过Proxy的静态方法来创建一个对象,最后调用这个对象的方法即可。比较烦的是如何创建一个代理对象,首先传入被代理对象的类的类加载器、它们共通的接口以及一个InvocationHandler实例(上面使用了匿名内部类,实际上还可以用lambda表达式),分别有三个参数,是代理的对象、执行的方法和方法所带的函数。
cglib提供的
由于JDK自带的代理需要某个类实现了接口才可以动态代理,就有了这个第三方的jar实现。
原理:运行时根据目标类动态创建代理类,代理类是目标类的子类。然后通过这个子类来实现父类中的方法。
1 | public static void main(String[] args) { |
实现代码几乎和上面的JDK的一样,只是少了一个接口,但是因为它是通过子类实现的,所以实现类中独有的那些方法也可以被加强,这是接口所做不到的。
优点:只要一个类它不是final的就可以被加强。
Spring中的AOP
作用:之前也讲到了,在实际中遇到重复的代码,我们的做法是抽取出来并且封装成函数来调用,但是这么做会造成方法之间的依赖,于是就有了Spring的AOP帮我们解决这个问题。注意,需要依赖aspectjweaver,请不要忘记添加这个依赖。
假设需要为某个类中的某个方法进行增强,比如为这个方法加上一个日志记录的功能。
1 | // 定义接口 |
现在需要为这里的三个方法增加一个日志记录的功能,所以首先需要去定义一个日志类,并在其内部完成功能:
1 | public class Logger { |
最后就是配置Spring,让它把我们定义的打印日志的功能,加入到上面三个方法中。
通过XML的方式
1 |
|
真心觉得特别简单,放入IOC容器这个是必须的,否则spring怎么帮你创建对象并管理呢?然后只要想好,哪个类的哪个函数,需要和另外一个类中的一个函数发生关系,是怎么样的关系即可。
通过注解的方式
首先是需要实现类成为一个组件(相当于声明为bean对象,此处略),然后需要修改下Logger类,为他加上几个注解即可:
1 |
|
xml文件:
1 |
|
最后再结合xml文件,只需要简单的定义扫描的包和开启AOP支持即可。虽然可以用类来使用纯注解,但是我个人认为还是xml结合注解会方便的多。
Spring JdbcTemplate
对JDBC进行了一些简单的封装。