Spring之IOC
为什么需要IOC
传统代码直接new
对象,耦合性太强,不方便以后扩展。在使用了IOC容器后,所有对象都被定义在了XML中,以后再要修改对象,仅需修改XML,而不必改换、编译Java代码,降低了耦合性。
what is IOC
将对象创建的权力交给第三方,即Spring IOC
容器,由Spring
管理对象的生命周期(即对象的 创建-使用-销毁)。
IOC:(Inversion of Control)控制反转,是面向对象编程的一种设计原则。其中最常见的方式是依赖注入(dependency injection)。
Spring的配置文件(application.xml)
application.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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--通过bean标签完成对象的创建-->
<bean id="user" class="com.jt.User"></bean>
</beans>
-
bean
标签:- id:Spring容器中对象的唯一标识,不可重复,一般为类名首字母小写
- class:类的全路径 包名.类名
测试容器创建对象
@Test
public void beanTest01(){
//1.通过加载配置文件,创建spring容器
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
//2. 从容器中获取对象
//2.1 通过id值获取对象
// User user =(User) context.getBean("user");
//2.2 通过class值(类)获取对象
User user = context.getBean(User.class);
//3.执行方法
user.say();
}
IOC原理
@Test
public void demo2() throws ClassNotFoundException,IllegalAccessException,InstantiationException{
//拿到类型
Class<User> clazz = User.class;
//通过类实例化类型
Class userClass = clazz.forName("com.jt.User");
//实例化对象
User user = (User)userClass.newInstance();
//执行方法
user.say();
}
-
当Spring程序执行时,会通过配置文件内容进行解析
-
Spring通过
反射
拿到实例化的对象(待补充) -
将实例化好的对象保存到超大的Map集合中<K,V>
bean中的id当作map的Key,实例化好的对象当作Value
Map<id,对象>
-
从容器当中获取对象,则从Map集合中通过id获取对象即可。
4.2 关于反射机制的补充说明
反射机制调用时,必然调用对象的无参构造,所以写构造方法时必须先写无参构造方法。
IOC之工厂模式(factory-method)
问题与应用场景
一般而言,Spring创建的对象是通过反射调用。但出于业务需要,我们需要实例化一个抽象类的对象或者复杂的接口的对象。
解决方案
Spring提供了
工厂模式
用于实例化复杂对象
静态工厂模式
静态工厂:里面的方法是静态的
补充:
静态方法特点: 1.静态方法可以通过==类名.==的方式调用
2.静态属性 内存当中独一份
静态工厂类
/**
* 静态工厂
*/
public class calendarFactory {
//获取抽象类的实例,静态工厂所以方法是静态的
public static Calendar getCalender(){
return Calendar.getInstance();
}
}
配置文件
<!--静态工厂写法 方法必须是static-->
<!--由于类中方法是静态的,故可以直接进行调用,使用 factory-method=方法名 即可-->
<bean id="calender1" class="com.jt.factory.calendarFactory" factory-method="getCalender"></bean>
测试静态工厂实例化抽象类
@Test
public void testGetCalender(){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Calendar calender1 = (Calendar) context.getBean("calender1");
System.out.println("当前时间 = " + calender1.getTime());
}
结果:当前时间 = Wed Apr 21 16:20:06 CST 2021
实例工厂模式
调用:对象.方法
实例工厂类
public class InstanceFactory {
public Calendar getCalendar(){
return Calendar.getInstance();
}
}
配置文件
说明:由于是实例工厂,里面的方法都不是static
的,所以工厂本身需要加入IOC容器当中,将其实例化后才可以进行方法的调用
factory-bean
:实例化的工厂对象
<!--1.将实例工厂加入bean之中-->
<bean id="instanceFactory" class="com.jt.factory.InstanceFactory"></bean>
<!--2.使用实例工厂调用方法-->
<bean id="calendar2" factory-bean="instanceFactory" factory-method="getCalendar"></bean>
测试实例工厂
@Test
public void testInstanceFactory(){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Calendar calender2 = (Calendar) context.getBean("calendar2");
System.out.println("当前时间 = " + calender2.getTime());
}
结果:当前时间 = Wed Apr 21 16:41:06 CST 2021
Spring工厂模式(FactoryBean)
静态工厂与实例工厂调用比较复杂,故spring提供了一个接口,方便我们实例化一些抽象类、复杂的接口
使用步骤:
- 实例化
FactoryBean
- 重写三个方法
- 源码中接口的
isSingleton
方法设置了default
,所以实现类可以不重写,根据需要即可。 getObject
:获取对象。getObjectType
:获取类型。isSingleton
:是否单例。
- 源码中接口的
- 将实例化的
FactoryBean
注册到IOC容器中
本质:
钩子函数:会自动地调用并且回调到主程序
FactoryBean
实际上就是暴露出来的接口,其包含三个钩子函数,在程序读取到FactoryBean
时,就会按顺序地、自动地去执行这些钩子函数,最终就将我们需要的抽象类、复杂接口的实现加入到IOC容器了。
单多例(scope)
实体类User
public class User {
public User() {
System.out.println("创建对象");
}
public void say(){
System.out.println("我是用户!!!");
}
}
application.xml
<!--测试单多例-->
<bean id="user" class="com.jt.pojo.User"></bean>
测试
@Test
public void testUser(){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
context.getBean("user");
context.getBean("user");
context.getBean("user");
//仅执行了一次构造方法
}
说明:
- Spring容器中默认的对象都是单例的(通过无参构造实例化对象)
- 有时需要通过多例对象为用户提供服务(数据源链接)
- 当在设置多例对象后,会自动开启懒加载,同时将对象的生命周期交给使用者来管理(随用随销)。
属性:scope
参数:
- singleton——单(默认)
- prototype——多
application.xml
<!--测试单多例-->
<bean id="user" class="com.jt.pojo.User" scope="singleton"></bean>
test
@Test
public void testUser() {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
context.getBean("user");//创建对象
context.getBean("user");//创建对象
context.getBean("user");//创建对象
}
懒加载(lazy-init)
懒加载:当用户获取对象时,容器才创建对象,称之为懒加载
说明:
- Spring容器中默认规则是
容器创建则对象创建
。若需要配置懒加载,则就在bean中添加lazy-init
属性值为true
。 - 一般服务器对象应该先行创建,用户直接使用,保证效率。而懒加载仅对于很少使用却又消耗大量内存的对象使用。
属性:lazy-init
参数:
- default:默认,关闭
- true:开启
- false:关闭
原则:只要是多例对象,都是懒加载
application.xml
<bean id="user" class="com.jt.pojo.User" lazy-init="true"></bean>
补充:idea之debug
设置端端后,F6
执行一条,F8
放行整个程序
生命周期
生命周期的过程:
- 实例化对象
- 初始化操作(为对象赋初值)
- 用户使用对象(调用其中方法)
- 对象销毁(一般都是释放资源
区别于销毁对象
)
User.java
public class User {
private String conn;
public User() {
System.out.println("创建对象");
}
//2.初始化方法
public void init(){
this.conn = "初始化赋值";
System.out.println("初始化资源:" + this.conn);
}
//3.用户调用的方法
public void say(){
System.out.println("我是用户!!!");
}
//4.销毁方法
public void destroy(){
this.conn = null;
System.out.println("释放资源:" + this.conn+"~~~~");
}
}
application.xml
<bean id="user2" class="com.jt.pojo.User" init-method="init" destroy-method="destroy"></bean>
属性:
- init-method:初始化会调用的方法
- destroy-method:销毁时会调用的方法
DI(依赖注入)
DI:(Dependencies Injection)依赖注入。即对象中的属性,应由Spring容器动态赋值。
测试依赖注入
两种方式:
- Set注入
- 构造方法注入
构建POJO属性
//为属性赋值的方式有2种 1.set/get 2.构造方法 alt+insert eclipse
public class User {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(){
}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
Set注入
application.xml
<!--管理user对象-->
<bean id="user" class="com.jt.pojo.User">
<!--调用对象的set方法实现赋值 set方法必须添加-->
<property name="id" value="101"></property>
<property name="name" value="李元芳"></property>
</bean>
测试
@Test
public void testDI(){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
User user = context.getBean(User.class);
System.out.println(user);
}
User{id=101, name='Jack'}
构造方法注入
application.xml
<!--构造方法注入-->
<bean id="user" class="com.jt.pojo.User">
<constructor-arg name="id" value="101" />
<constructor-arg name="name" value="Rose" />
</bean>
测试同上
User{id=101, name='Rose'}
属性注入高级用法
为各种数据类型赋值:
- List
- Set
- Map
- Properties
User.java
//为集合赋值
private List list;
private Set set;
private Map map;
private Properties pro; //内部仅能保存String类型数据
//getter and setter...
application.xml
<!--Set方法注入各种类型-->
<bean id="user" class="com.jt.pojo.User">
<property name="id" value="101"></property>
<property name="name" value="Jack"></property>
<property name="list">
<list>
<value>张三</value>
<value>李四</value>
<value>王五</value>
</list>
</property>
<property name="set" >
<set>
<value>1</value>
<value>2</value>
<value>3</value>
</set>
</property>
<property name="map">
<map>
<entry key="id" value="1000" />
<entry key="name" value="Mack" />
</map>
</property>
<property name="pro">
<props>
<prop key="proId">110</prop>
<prop key="proName">Green</prop>
</props>
</property>
</bean>
补充:
- 集合类型赋值都是在property标签体中声明集合标签然后进行赋值
- Map中每个元素都是一个
Entry
,故注入是是以Entry为单位赋值 - Properties属性中也是
K,V
,但它的K写在属性中,V写在标签中
test
User{id=101, name='Jack', list=[张三, 李四, 王五], set=[1, 2, 3], map={id=1000, name=Mack}, pro={proName=Green, proId=110}}
定义公共的map标签
添加头文件引用
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="... http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
application.xml
<util:map id="map">
<entry key="id" value="1001" />
<entry key="name" value="Brown" />
</util:map>
<bean id="user" class="com.jt.pojo.User">
<property name="map" ref="map">
</property>
</bean>
test
User{map={id=1001, name=Brown}}
小结:使用util工具包定义map集合,指定id。然后在property标签中使用ref
属性引用
Spring容器管理3层代码结构
User.java
private Integer id;
private String name;
//getter and setter..
//toString...
dao
UserDao
public interface UserDao {
void addUser(User user);
}
UserDaoImpl
public class UserDaoImpl implements UserDao {
@Override
public void addUser(User user) {
System.out.println("添加用户:"+user);
}
}
service
UserService
public interface UserService {
void addUser(User user);
}
UserServiceImpl
public class UserServiceImpl implements UserService {
private UserDao userDao;//基于Spring注入dao 面向接口编程
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void addUser(User user) {
userDao.addUser(user);
}
}
controller
public class UserController {
private UserService userService;
//由前端传过来,暂时先这么模拟
private User user;
public void setUser(User user) {
this.user = user;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
public void addUser(){
userService.addUser(user);
}
}
说明:
程序运行时,Spring容器为controller
类引用了service
的实现类,而service
类引用了dao
的实现类。dao
的实现类为user
实体赋了初值。
在controller
调用setUserService
后,会找到下层service
层的接口,然后去找实现。同理也会往下找dao
层的接口,然后实现,最后将实现类赋值进去。
application.xml
<!--1.构建User对象-->
<bean id="user" class="com.jt.pojo.User">
<property name="id" value="100"></property>
<property name="name" value="springMVC设计模式"></property>
</bean>
<!--2.构建Dao对象
根据面向接口编程
Id写的是接口名称
class:实现类的包路径
-->
<bean id="userDao" class="com.jt.dao.UserDaoImpl">
</bean>
<!--3.构建Service对象-->
<bean id="userService" class="com.jt.service.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<!--3.构建Controller对象-->
<bean id="userController" class="com.jt.controller.UserController">
<property name="userService" ref="userService"></property>
<property name="user" ref="user"></property>
</bean>
test
@Test
public void test01(){
//启动容器,紧接着Controller、ServiceImpl、DaoImpl、User类都被实例化了
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("application.xml");
//拿到Controller,
// 由于controller类中有属性UserService,而在bean中对这个属性有ref=UserService,故会调用setUserService方法注入进来
//而Service类中有属性UserDao,而在bean中对这个类注入了ref=UserDao,故会用setUserDao方法注入进来
//controller层使用set注入赋值了IOC容器中的User类
UserController controller = (UserController) context.getBean("userController");
//模拟前端传过来的对象数据
// User user = new User();
// user.setId(1000);
// user.setName("张三");
// controller.setUser(user);
//controller层调用addUser,则会去找Service层的addUser
//然后再往下找Dao层的addUser,最终执行了SQL语句
controller.addUser();
}
**补充:**Spring中规定,如果getBean()
传入的是接口类型,则自动查找/注入该实现类。**但是!**该接口必须有且仅有一个实现类。
if(getBean(isInterface)){
Class targetClass = interface.getImpl();
根据类型,动态获取对象
return 对象
}
特殊字符说明
核心思想:转义。
方法:
-
使用转义字符
转义字符 实际符号 描述 <
< 小于 >
> 大于 &
& 和号 '
’ 单引号 "
" 引号 -
万能转义字符(不能写在属性内,需要写在标签体当中)
<![CDATA[xxx任意字符]]>
注解开发与自动装配
自动装配
说明:程序无需手动编辑property属性。
属性:autowire
参数:
-
byName:根据属性的名称进行注入(通过方法名推导)
- 找到对象的所有set方法 setUserDao()
- setUserDao -> set去掉 -> UserDao -> 首字母小写 -> userDao属性
- spring会根据对象的属性名查询自己维护的Map集合,根据userDao名称,查找Map中的Key与之对应,如果匹配成功,则能自动调用set方法实现注入**(必须有set方法)**
-
byType:根据属性的名称进行注入(通过方法参数类型推导)
- 找到对象所有的set方法
- 根据set方法找到参数类型UserDao.class
- 使用这个类型与spring中维护的对象类型进行匹配,最终set方法注入
-
default:采用父级标签(即beans的default-autowire属性)的配置。
byName
<bean id="userService" class="com.jt.service.UserServiceImpl" autowire="byName">
</bean>
注解模式
说明:简化XML配置方式,使用注解
四种加入容器注解:
@Controller
:标注这个类是Controller层,并加入容器
@Service
:标注这个类是Service层,并加入容器
@Repository
:标注这个类是Repository层,并加入容器
@Componet
:万能注解
注意:使用了注解后,仍需配置xml文件,开启包扫描,才可以将带有注解的类加入容器中
头信息
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
application.xml
<context:component-scan base-package="com.jt" />
属性:use-default-filters
参数:
- true 默认,扫描所有
- false 按用户指定
情景1:只想扫描某个注解
关键字:context:include-filter
<context:component-scan base-package="com.jt" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
情景2:不想扫描某个注解
关键字:context:exclude-filter
<context:component-scan base-package="com.jt" use-default-filters="false">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
说明:上面的注解与包扫描仅仅是将类加入到了Bean中,还没有进行属性的注入
依赖注入注解:
@AutoWired
:通过类型/名称将容器中的对象注入到属性中,首选类型,若类型不能匹配,则按名称注入。
@Qualifier
:结合@AutoWired
使用,将方式改为仅通过属性名称注入。可以设置参数value="xx"
来自定义属性名字
@Resource
:java原生提供,支持byType
和byName
,参数有type=" "
和name=" "
方法:
上述的属性注入在调用时,自动地封装了Set方法,所以Set方法可以省略不写。
配置类
说明:为了抛弃XML配置文件,使用配置类来进行各项配置。
注解:
@Configuration
——标识我是一个配置类,相当于application.xml
@ComponentScan
——包扫描,参数value="xx"
加载:类AnnotationConfigApplicationContext([Class<T>...])
测试:
// 测试全注解开发的MVC设计模式
@Test
public void AnnotationTest(){
ApplicationContext context =
new AnnotationConfigApplicationContext(SpringConfig.class);
UserController controller = context.getBean(UserController.class);
controller.addUser();
}
关于接口多实现类的几点说明
原则:Spring原则上一个接口只有一个实现类
需求:Service层需要两个实现类
- 编辑实现类A
- 编辑实现类B
- 为A、B设置自定义的ID
- 在依赖注入的地方使用
@Qualifier
注解指定依赖注入的对象ID
Spring容器在内存中的执行顺序
- 当程序开始执行,读取配置类,首先调用Controller类的无参构造,尝试实现这个类
- 然后发现需要实现下面的属性Service,于是实例化Controller暂时
挂起
- 于是程序便去实例化Service,发现需要实例化Dao,于是也
挂起
- 继续下探,实例化Dao,然后再回头实例化Service、Controller
容器管理业务数据
注解:@Bean
作用:可以将业务数据实例化,并交由Spring容器管理,但是@Bean
注解要写到配置类中。
SpringConfig.java
/*执行@Bean的方法 将方法名称当作ID,返回值的对象当作value 直接保存到Map集合中*/
@Bean
public User user(){
User user = new User();
user.setId(101);
user.setUsername("test @Bean");
return user;
}
相当于在xml中写了<bean id="user" class="user.class">....</bean>
UserController
...
@AutoWired
private User user;
...
从配置文件当中取出值放入对象
user.properties
user.id=102
user.username=小明
SpringConfig.java
private Integer id;
private String username;
注解:@PropertySource()
加载指定的配置文件,将数据保存在Spring容器中
参数:value="classpath:/xx"
SpringConfig.java
@PropertySource("classpath:/user.properties")
public class SpringConfig {
...
}
注解:@Value()
给属性赋值使用,参数value="xx"
设置Key
注意:在Spring容器中查找key=user.id
的数据时,应使用EL表达式:${user.id}
细节:配置文件默认是ISO-8859-1编码,所以在@propertySource
注解要添加encoding="utf-8"
AOP
What is AOP
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
知识扩展:
语言——
- 面向过程
- 半面向对象 C++
- 面向对象
编程方式——
- 面向接口
- 面向切面
作用:在不修改原有代码的条件下 对方法进行扩展
事务控制(铺垫)
需求:insertUser
要和insertDept
同时完成
核心部分
try {
System.out.println("事务开始");
userMapper.addUser(user);
System.out.println("事务结束");
}catch (Exception e){
e.printStackTrace();
System.out.println("事务回滚");
}
缺点:
- 事务管理代码与业务代码紧紧地耦合在了一起,不便于维护和阅读。
- 代码冗余,不便于大批量的开发。
解决方案:采用代理模式进行编辑。
代理模式
生活中的代理
- 租房中介
模型:
-
暴露一个公共的接口(租房子)
-
客户与中介机构进行沟通,中介看起来和房东功能一致。
-
完成用户额外的操作(收取中介费)
-
组成部分
- 要求代理者实现与被代理者相同的接口
- 在代理方法中实现功能的扩展
- 用户调用代理对象完成功能(用户认为代理就是目标对象)
-
调用流程
静态代理
代理实现事务控制
角色划分:
- 目标对象target UserServiceImpl
- 目标方法 method addUser()
- 代理:实现事务的控制。
- 代理对象与目标对象实现相同的接口
UserServiceImpl.java
目标方法的实现类另指定ID
@Service("target")
public class UserServiceImpl implements UserService {
...
}
StaticProxy.java
-
偷梁换柱,将代理类ID改为
userService
-
代理类要完成事务控制并且执行目标对象方法
@Service("userService")
public class StaticProxy implements UserService {
@Autowired //首先会ByType,然后再ByName
private UserService target;
@Override
public void addUser(User user) {
try {
System.out.println("事务开始");
target.addUser(user);
System.out.println("事务结束");
}catch (Exception e){
e.printStackTrace();
System.out.println("事务回滚");
}
}
}
test
调用ID是userService
的bean
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
UserService service = (UserService) context.getBean("userService");
User user = new User();
user.setId(101);
user.setUsername("测试代理事务");
service.addUser(user);
静态代理缺点:
- 每对一个类做代理,都要实现一个代理实现类,不够通用
- 类中一个方法仅针对一个业务进行解耦。
动态代理
- JDK代理
- 目标要求:目标对象实现接口
- 代理要求:代理对象实现与被代理者相同的接口
- 关系:兄弟关系
- CGLib代理(了解)
- 目标要求:不管目标对象是否有接口,都可以为其创建代理对象
- 代理要求:代理对象必须继承目标对象
- 关系:父子关系
JDK动态代理
利用(静态)工厂模式创建代理对象
JDKProxyFactory.java
类加载器:在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。故这里获取到了限定target
这个类的对象。
例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
//能否利用一个工厂动态为目标对象创建代理
public class JDKProxyFactory {
//关于匿名内部类用法说明:匿名内部类引用外部参数 要求参数必须final
public static Object getProxy(final Object target){
/*
*参数分析:
* 1.ClassLoader classloader 获取类加载器(获取目标对象的Class)(类似.class、getClass、forName)
* 2.Class<?> [] interfaces获取类的接口数组,jdk代理要求必须有接口
* 3.InvocationHandler h 对目标方法进行扩展
* */
//1.获取类加载器
//在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识
ClassLoader classLoader = target.getClass().getClassLoader();
//2.获取接口数组
Class[] interfaces = target.getClass().getInterfaces();
Object proxy = Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//目标方法执行的返回值
Object result = null;
try {
System.out.println("事务开始");
result = method.invoke(target,args); //执行目标方法
System.out.println("事务结束");
return result;
}catch (Exception e){
e.printStackTrace();
System.out.println("事务回滚");
}
return result;
}
});
return proxy;
}
}
重要知识点:
- 关于匿名内部类用法说明:匿名内部类引用外部参数 要求参数必须final
- 匿名内部类的
invoke
方法说明:当代理对象执行时,“回调”该方法。 - 目标方法执行
metho.invoke(method,args)
test
@Test
public void txJDKProxyTest(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
UserService target = (UserService) context.getBean("target");
UserService userService = (UserService) JDKProxyFactory.getProxy(target);
User user = new User();
user.setId(101);
user.setUsername("测试JDK动态代理");
//代理对象在调用目标方法时会触发回调
//触发了回调,回调函数会获取到这个方法名以及参数,传给钩子函数invoke
userService.addUser(user);
}
执行流程:
- 获取目标对象
- 根据目标对象获取代理对象
- 使用代理对象调用方法
动态代理优势:
- 使用工厂设计模式,根据传入的对象动态地获得代理对象
- 根据代理对象调用的方法,动态地执行方法
- 故动态代理更为通用
AOP名词介绍
- 连接点:用户可以被扩展的方法(每个写好的业务方法) joinPoint
- 切入点:用户实际扩展的方法(代理调用的方法) pointcut
- 通知:扩展方法的具体实现(相当于代理类中的方法) @Before
- 切面:将通知应用到切入点的过程(相当于整个代理类) 切面=切入点表达式+通知方法(方法功能得到扩展的全部配置)
通知的类型
- before 目标方法前执行
- afterReturning 目标方法执行后返回时执行
- afterThrowing 报错后执行(catch)
- after 目标方法执行完后执行(finally)
- around 环绕通知
小结:
- 环绕
around
通知时处理程序的首选,因为它可以控制业务是否执行(修改程序执行轨迹) - 另外的四大通知多用来对程序进行监控(监控系统)
切入点表达式
概念:当程序满足切入点表达式,才能进入切面,执行通知方法。
- bean(“bean’s id”) 根据bean的id进行拦截
- within(“包名.类名”) 可以使用通配符(* ?)
上述切入点表达式 粒度时类级别的。粗粒度
- execution(返回值类型 包名.类名.方法名(参数…))
控制的是方法参数级别。所以粒度较细。最常用
- (Spring3以后)@annotation(包名.注解名) 只拦截注解。
注解是一种标记,根据规则标识某个方法/属性/类 细粒度
AOP注解
aop类
类
@Aspect
:标识该类为AOP切面,Spring容器默认不能识别切面注解,需要手动配置
方法
@Before
:标记前置通知,参数为切入点表达式
@Ponitcut
:定义切入点表达式,其他方法可以引用
config配置类
@EnableAspectJAutoProxy
:启动aop注解,创建代理对象
proxyTargetClass=
- true 强制使用CGLIB
- false 默认,使用JDK
切入点表达式
/**
* 切入点表达式练习
* within:
* 1.within(com.jt.*.DeptServiceImpl) 一级包下的类
* 2.within(com.jt..*.DeptServiceImpl) ..代表多级包下的类
* 3.within(com.jt..*) 包下的所有的类
*
* execution(返回值类型 包名.类名.方法名(参数列表))
* 1.execution(* com.jt..*.DeptServiceImpl.add*())
* 注释: 返回值类型任意的, com.jt下的所有包中的DeptServiceImpl的类
* 的add开头的方法 ,并且没有参数.
*
* 2.execution(* com.jt..*.*(..))
* 注释: 返回值类型任意,com.jt包下的所有包的所有类
* 的所有方法 任意参数.
*
* 3.execution(int com.jt..*.*(int))
* 4.execution(Integer com.jt..*.*(Integer))
* 强调: 在Spring表达式中没有自动拆装箱功能! 注意参数类型
*
* 注解形式: @annotation(包名.注解名)
* @Before("@annotation(com.jt.anno.Cache)")
*/
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface Cache {
}
动态获取目标方法信息(before)
说明:Spring为了动态地获取目标方法的各种信息,实现设置了JoinPoint
对象用以获取。使用时只需要在切入点传入即可。
方法名 | 功能 |
---|---|
Signature getSignature(); | 封装了署名信息的对象,可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取目标方法参数 |
Objet getTarget(); | 获取被代理的对象 |
Signature
方法名 | 功能 |
---|---|
getDeclaringTypeName() | 返回声明类名 |
getName() | 返回方法名 |
test
@Before("@annotation(com.jt.anno.Cache)")
public void before(JoinPoint joinPoint){
System.out.println("获取被代理的对象:"+joinPoint.getTarget().getClass());
System.out.println("获取目标对象类名:"+joinPoint.getSignature().getDeclaringTypeName());
System.out.println("获取目标对象方法名:"+joinPoint.getSignature().getName());
System.out.println("获取目标对象参数:"+ Arrays.toString(joinPoint.getArgs()));
System.out.println("前置通知");
}
记录方法返回值(afterReturning)
说明:注解AfterReturing
中有参数returning
可以指定返回值的形参。然后在方法中使用这个形参即可获取到方法执行后的返回值。
aop.java
@AfterReturning(value = "pointcut()",returning = "result")
public void AfterReturning(Object result){
System.out.println("afterReturning通知");
System.out.println("用户的返回值结果:"+result);
}
补充:
- 当JoinPoint和返回值共用时,连接点对象必须位于第一位
- Spring在进行参数赋值时,采用index[0]赋值
- 报错提示 ::0
日志记录报错信息(afterThrowing)
说明:当程序发生异常时,需要捕获异常并将其记录到日志当中。可以使用AfterThrowing
注解配合throwing
参数将异常返回值接收下来。
aop.java
/**
* 记录报错信息
* @param e
*/
@AfterThrowing(pointcut = "pointcut()",throwing = "e")
public void AfterThrowing(Exception e){
System.out.println("异常信息:"+e.getMessage());
System.out.println("异常类型:"+e.getClass());
}
记录程序运行(around)
说明:在目标方法前后执行,是前四个的集合体。
特殊:
around
环绕通知方法需要返回一个Object
- 需要传入一个
ProceedingJoinPoint
参数。 - 执行目标方法时使用上述类的
proceed
方法(实际就是invoke
) - proceed包含了
执行下一条通知
、执行目标方法
、接收返回值
三个功能 - 需要
try/catch
将执行目标方法的部分括起来防止目标方法执行出错导致程序崩溃
关于通知方法执行顺序的细节
- around开始
- before
- 目标方法
- afterReturning
- after
- around结束
order改变多个切面类执行顺序
说明:默认按照文件创建顺序执行切面。可以通过@Order(数字)
来改变切面的执行顺序,数字越小优先级越大
aop1
@Component
@Aspect
@Order(2)
public class AOP1 {
@Before("@annotation(com.jt.anno.Cache)")
public void before(){
System.out.println("AOP1执行");
}
}
aop2
@Component
@Aspect
@Order(1)
public class AOP2 {
@Before("@annotation(com.jt.anno.Cache)")
public void before(){
System.out.println("AOP2执行");
}
}
test
AOP2执行
AOP1执行
更新部门