Spring5
IOC
IOC本质
控制反转IoC(inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方式。
在没有IoC的程序中,对象的创建与对象之间的关系完全是由背后的程序控制,控制反转后会将对象的创建转移给用户。
之前在MyBatis的学习中,采用XML方式配置Bean,Bean的定义信息和实现是分离的,然而采用注解可以将实现整合到Bean中,这样就达到了零配置的目的。
IOC代码实现
源码下载:Spring-ioc.rar
非IOC思想
先在com.theo.dao
下创建UserDao
接口类
1 |
|
对应com.theo.ropo
下创建User
的JavaBean
1 |
|
这次不同于MyBatis的以配置文件方式实现接口类方法,我们创建实现类UserDaoimpl
在dao
包下:
1 |
|
在测试类中:以传统方式我们需要UserDao user = new UserDaoimpl();
后才能调用GetUser()
。这或许看起来很合理,因为我们都一直是这么做的。但试想顾客要改变需求,我们需要新增UserDaoimpl2
,然后在测试类中重新new对象再调用方法。这就造成了需要频繁更动测试类代码,主要是因为要new对象。
Spring的Bean容器可以在配置文件帮你实例化各种类,然后测试类中就只需要从容器中取对象即可。
bean.xml
1 |
|
在resource
下新建bean.xml
(名字随意),先填写以上配置内容,说明该配置文件下的Bean都会存放在Spring容器中。
通过<bean>
标签可以实例化各种类(不只是JavaBean,还可以是Dao层的实现类等等)
1 |
|
id
就是实例化类的对象名,class
就是需要实例化的类名,需要填写全限域名。等于说一个bean标签就做了UserDao UserDaoimpl = new UserDaoimpl();
这么一件事。
对于没有变量的类,bean标签只需要指明id和class即可。对于有变量的类,需要通过property标签一一赋值,特殊的:如果变量类型也是类,则需要通过
ref="id"
来赋值,其中id
就是已经被实例化的类,如UserDaoimpl
。
配置完bean标签后,我们来看下相比传统方法,我们如何调用GetUser
方法:
1 |
|
第一步是获取Bean容器,其中有各种已经实例化好的对象。第二步通过id直接获取对象,默认返回Object,需要强转类型。
可能现在并不能看出这种方式能有什么特别优秀的地方,如果项目中再新添Controller层(用来调用Dao层方法),这样测试类中只需要拿Controller层的对象,然后如果用户需要使用UserDaoimpl2
的功能,就只需要在bean.xml
的Controller的bean标签中修改ref值。这样测试类中的代码完全不需要动!!!
这样就展现出了与传统方法不同的点,也就是IoC的基本思想:对象由Spring来创建,管理,装配!
Spring配置
- 别名
1 |
|
通过取别名,getBean()
就能通过别名来取,可以去多个别名,之间用逗号分隔。
- import
如果多人开发不同类,分别写了不同bean.xml
,整合这些配置文件时只需要在主配置文件applicationContext.xml
中<import resource="bean.xml">
,同时创建Bean容器时改成applicationContext.xml
即可获取所有子配置文件中的对象了。
依赖注入
我们创建一个拥有各种类型的属性的JavaBean:
1 |
|
注意一定要配有Setter方法和无参的构造方法,因为依赖注入是通过无参构造对象,Setter方法注入属性值
Set注入
这样对应不同的类型有不同的注入方式,Set注入参照以下:
1 |
|
构造器注入
如果实体类的属性没有配置Setter方法,配置属性是依靠有参构造方法实现的,Set注入将无法使用:
1 |
|
c命名/p命名空间注入
- c命名空间
先在beans标签头部导入xml约束。
1 |
|
c命名空间适用于简单的属性注入:如基本类型,要求JavaBean中有无参构造方法
1 |
|
- p命名空间
先在beans标签头部导入xml约束。
1 |
|
p命名空间适用于简单的属性注入,但必须要求JavaBean中有无参,有参构造方法!!
1 |
|
Bean作用域
单例模式(默认模式)
1 |
|
这样重复通过getBean()
获取的对象都是同一个对象。
原型模式
1 |
|
这样每次通过getBean()
获取的对象都是不同对象,可能出现浪费资源的现象!!
Bean自动装配
Spring会根据上下文,自动为Bean装配属性值。
以之前装配Student
的address
为例,原先是通过<property name="address" ref="address"/>
注入,因为上下文是有已经装配好的address
对象的,我们就可以让Spring自动去寻找并自动装配。
- byName
1 |
|
自动装配的原理是利用了Student类中的Setter方法,对于address属性有
1
2
3
public void setAddress(Address address) {
this.address = address;
}那么Spring就会在Bean配置文件中寻找有没有id=”address“的对象,有则自动注入。但是如果id取名为了addressTan或其他,这种自动装配就不起作用了!
- byType
1 |
|
这种会在上下文查找Address类的对象,但是如果有多个同类对象,这种方法就不生效
通过自动装配后,就不需要写<property name="address" ref="address"/>
了。
注解实现自动装配
- 导入xml约束
1 |
|
- 配置注解的支持
1 |
|
- @Autowired
@Autowired注解要添加在需要装配的实体类属性上,或该属性的Setter方法上。
1 |
|
1 |
|
注意@Autowired默认通过byType装配,如果有多个同类型的Bean存在,就无法装配。这时再添加注解**@Qualifier(value=”xxx”)**去帮忙指定配置掉一个id="xxx"
的Bean,相当于byName的Autowired。
- @Resource
@Resource注解无需配置Spring即可使用,它会先通过byType查询,如果找到多个同类型的Bean就会从中找出id=“属性名”的,如果也没找到就会报错。有点像byName与byType的聚合。
Spring注解开发
- 使用注解需要在
applicationContext.xml
导入context的xml约束,同上。
1 |
|
- 指定需要扫描的包,该包下的注解才能生效
1 |
|
装配Bean
在pojo
下新建实体类User.class
,设置属性public String name = "TZQ";
。在类上添加注解@Component
,之后类旁就会出现Spring Bean装配成功的绿叶图标。
注意自动装配给该Bean分配的id=“user”
,在测试类通过context.getBean()
填入类名的小写user
!!!!
注入Bean
通过xml注入是通过<property>
来注入的,注解只需要@Value()
即可。
1 |
|
但你会发现,这种会把代码写死。如果需要更改值就需要更改代码,而不是更改配置文件了。除非非常简单的注入或者不涉及更改值的注解(@Autowired),否则不建议使用。
衍生注解
@Component有多个衍生注解,对于Web应用的MVC三层都有其独特的注解,但功能一样!!
- dao层:【@Repository】
- service层:【@Service】
- controller层:【@Controller】
这三个注解都实现了将被注解的类装配到IoC容器,前提是在
applicationContext.xml
中加上扫描包路径!!
JavaConfig代替配置文件
使用JavaConfig可以完全删除
applicationContext.xml
,装配Bean的任务全权由JavaConfig负责
@Bean
新建com.theo.javaconfig.JavaConfig.class
:
1 |
|
@Configuration
表明该配置类会被Spring托管(也会将该类作为Bean装配到IoC容器中)。
@Bean
说明会把下面的方法装配成Bean放到IoC容器中!!方法名就是id,返回值类型就是class。获取该Bean有些许不同:
1 |
|
所以说,@Bean
装配了一个方法,但你从容器取出来的是一个Bean(返回类的对象),可以换一种角度来看,User没有装配到IoC容器中,而是GetUser自己实例化了User类再放到IoC容器中。这样想的话,User类就不用使用注解@Component
了。
但是JavaConfig也能发现一些缺点:
- User没有装配到IoC容器,这样就意味着Spring不会去管理User了
- JavaConfig只负责装配,但并不负责注入属性!!当需要注入时还是需要到User属性上使用
@Value()
的。你也可以设置有参构造方法,new的时候添上属性值,但这个方法不推荐。
如果你觉得GetUser作为id不适应,你也可以通过
@Bean("user")
来改变id。
@ComponentScan(“com.theo.pojo”)+@Component
那如果不使用@Bean,想像按照xml方式一样将User装配到IoC容器中呢?
1 |
|
这样我们再重新在User上添加注释@Component
,User就被装配到IoC容器中了。
1 |
|
照样能打印出内容。
综上,使用JavaConfig装配Bean有以下两种方式:
- 只使用@Bean,通过getBean()来实例化User
- 使用@ComponentScan(“com.theo.pojo”)+@Component,这样就直接将User装配到IoC
以上两种方式的效果都一样,也可以一起用,只是User会被实例化两次,但留在IoC的只是同一个对象,因为默认使用单例模式,故而getBean(“user”)和getBean(“GetUser”)获取的对象都是同一个,打印的hashcode()都一样。
代理
动态代理
实现动态代理的步骤
- 创建接口,定义目标类要完成的功能
- 创建目标类以实现接口
- 创建InvocationHandler接口的实现类,在invoke方法中完成代理类要做的功能:1.调用目标方法 2.添加功能
- 使用Proxy类的静态方法,创建代理对象,并把返回值强转为接口类型。
场景呈现
Client租房的时候如果不能直接与Host联系,就需要找到中介。首先,中介必须要提供Host提供的服务如:出租,允许租客看房等等。那么Host就要先通过一个接口来具体实现这些服务,中介就利用反射机制来调用这些服务(method.invoke),但中介也可以自己收取些小费,最后一同将服务提供给租客。
因而这一整套租房流程只按照实线流程进行,中途Host从没有参与。之后Host如要增添服务,只需要在功能接口增添并在目标类具体实现即可,那么代理就会自动拥有这些服务。
代码实现流程
源码下载: Spring-proxy.rar
- 定义功能接口
首先Host只允许租房和看房的服务,在com.theo.Service
下创建Rent
接口:
1 |
|
- 目标类实现接口
实例化一个Host,因为不同房主的租房规则不一样,我们就需要具体实现这些接口,在com.theo.Factory
创建Host
:
1 |
|
- 定义代理要提供的服务
InvocationHandler接口的实现类就是中介要实现的服务,附加收费和提供目标类服务都在invoke中实现。在com.theo.Handler
下创建RentHandler
1 |
|
因为执行目标服务是通过反射机制实现的,这样我们就需要获取目标类的Class,再利用Class的method域来调用目标类的具体服务。
通过反射的知识,我们知道invoke(target,args)
是要传入具体对象(Host)的,考虑到具体情况会有多个Host,我们就不在这一层指定好Host,而是通过有参构造方法让真正的的代理传入需要代理的具体对象(Host1/Host2…),这样应用面更广。
- 创建代理对象
之前的RentHandler
并不是真正的代理,只是定义了一个代理需要做的事,你可以看作是中介公司,在这里的中介都要遵从公司定义的规则:1.提供Host的服务 2.收取中介费用$20
接下来我们创建一个实实在在的中介,在com.theo.Proxy
下创建agency
:
1 |
|
首先明确我们要代理的对象是Host,将其创建出来:Rent factory = new Host();
。其后传入代理对象到中介公司,让中介公司知道Host提供的服务有哪些:InvocationHandler handler = new RentHandler(factory);
。之后通过这个中介公司创建出一个中介:Rent proxyInstance = (Rent) Proxy.newProxyInstance(factory.getClass().getClassLoader(), factory.getClass().getInterfaces(), handler);
,注意这里的三个参数都是固定的,如果要换一个目标类,只需要改成new Host1()
。最后代理就能直接调用服务了。
代理调用服务:proxyInstance你就可以直接想成是Host的对象,就可以直接调用Host实现的接口。
代码底层实现的原理:实际上proxyInstance.check(20210815)会去调用RentHandler的invoke方法:
check
就是参数method;20210815
就是参数args,再利用反射机制,method.invoke(target, args)
就是new Host().check(20210815)
。目标服务执行完后就是中介方自己添加的服务了。
- 执行agency
运行agency.class得到结果:
1 |
|
AOP
源码下载:Spring-AOP.rar
面向切面编程
通常情况下我们都是纵向开发,就像之前的租客与房主的例子,写完房主的相关逻辑代码后,租客就可以直接调用了,如果房主的某一服务需要做细微的调改,比如租房的金额增加,那么房主的相关代码就要做更改。这相当于更改了底层代码。如果中间加了个中介,加钱的事由中介负责,那么房主的代码就无需更改。
这就是面向切面编程的雏形:不更改业务逻辑的情况下,细微调整服务逻辑。
不知道有没有注意到动态代理的一个问题:通过代理调用目标类哪种方法,代理额外做的工作都相同,即收取$20费用。如果代理租房业务收取30,而代理看房业务只收20呢?这就是AOP要解决的问题了。
自定义类实现AOP
现在pom.xml
中导入依赖:
1 |
|
为实现以上的不同收费方式,在com.theo.Advice
下新建fares
:
1 |
|
以查房服务为例,当调用目标类的方法check()时,我们需要切入一个方法check_fare()。因此,在check()中制造切点(applicationContext.xml中配置)
1 |
|
先导入xml约束xmlns:aop="http://www.springframework.org/schema/aop"
以及xsi:schemaLocation
切点
<aop:pointcut id="check_pointcut" expression="execution(* com.theo.Factory.Host.check(int))"/>
这就是一个切点,表明将会在这个点插入方法,说白了切点就是一个方法。
id="check_pointcut"
指明切入点的名称,expression="execution()"
指示切入点的具体位置
execution表达式
execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。
在上面的例子中,第一个*
表示匹配所有返回类型,第二部分com.theo.Factory.Host.check
表明切点在这个方法中,以后执行该方法时就会附加上切面方法。第三部分check(int)
精准匹配到参数为int类型的方法,这可以用来区分重载方法;如果想囊括所有重载方法,int就可以用..
替代。
通配符 *****
如果我想在目标类中所有方法中加入相同切点,就需要使用com.theo.Factory.Host.*(..)
。同样的,使用com.theo.Factory.*.*(..)
就是在所有目标类的所有方法中加入同一切点。
com.theo.Factory.*.*
和com.theo.Factory..*
同一作用,表示包、子孙包下的所有类
切面与通知
如上,com.theo.Advise.fares
就是一个切面,面上有多个方法rent_fare()
、check_fare()
,每一方法称为通知
标记切面
先将切面实例化并交予IoC容器,再标记为切面:
1 |
|
1 |
|
在切点上接入通知
1 |
|
之前创建了check_pointcut
和rent_pointcut
两个切点,分别接入check_fare
和rent_fare
两个通知,这样执行Host.check(int date)
会附加执行fares.check_pointcut()
,Host.rent()
同理。
<aop:before 是指在执行完目标类方法前再执行通知。<aop:after 是指在执行完目标类方法后再执行通知
运行
编写测试类,以getBean("host")
获取Host对象,执行host.check(20210815)
执行结果:
中介收取查房费用$10
执行了目标类Host:房东允许你在20210815看房
Spring API实现AOP
Spring API常见切面有日志和验证。创建com.theo.Advice.log
1 |
|
这里选择前日志,它会在目标类方法执行前打印日志。我们打算对所有方法都加入日志:
1 |
|
1 |
|
注解实现AOP
注解实现是基于自定义类实现改进的。也需要经历标记切面、创建切点、接入通知。
创建com.theo.Advice.Annotation
:
1 |
|
@Aspect
表明该类为一切面,等价于:<aop:aspect ref="fares">
@Before("execution(* com.theo.Factory.Host.*(..))")
创建切点,同时声明执行先后。等价于:<aop:before method="check_fare" pointcut-ref="check_pointcut"/>
最后需要在applicationContext.xml开启注解支持和Bean装配。
1 |
|
至此,注解实现完成。
Mybatis-Spring
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!