Spring学习笔记·Spring中IoC、DI、AOP学习


写在前面, applicationContext.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>

一、Bean

1. IoC和DI

IoC(Inversion of Control,控制反转)。对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。
DI(Dependency Injection,依赖注入)。IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。
IOC和DI的关系简单来说就是IOC是方法论,DI是实现手段。

2. IoC容器

在 Spring IOC 容器读取 Bean 配置创建 Bean 实例之前, 必须对它进行实例化。只有在容器实例化后, 才可以从 IOC 容器里获取 Bean 实例并使用。
spring 提供了两种类型的 IOC 容器实现 BeanFactory 和 ApplicationContext 。
BeanFactory和ApplicationContext区别:
BeanFactory 是懒加载,第一次getBean时才会初始化Bean。
ApplicationContext是对BeanFactory扩展,提供了更多功能。

  • BeanFactory面向spring本身, 是spring框架的基础设施
  • applicationContext面向开发者, 几乎所有场景都直接用ApplicationContext

3. 配置Bean

3.1 三种实例化Bean的方式

  1. 使用构造器实例化(必须提供无参数构造方法)

applicationContext.xml

<bean id="userService" class="com.ttc.serviceImpl.UserServiceImpl" />
  1. 使用静态工厂方法实例化(简单工厂模式)

applicationContext.xml

<bean id="userService" class="com.ttc.service.UserServiceFactory" 
factory-method="createUserService"/>

UserServiceFactory.java

public class UserServiceFactory {
	public static UserService createUserService() {
		return new UserService();
	}
}
  1. 使用实例工厂方法实例化(工厂方法模式)

applicationContext.mxl

<bean id="userServiceFactory"
class="com.ttc.Service.UserServiceFactoryMethod" />
<bean id="userService1" factroy-bean="userServiceFactory"
factory-method="newUserService1" />
<bean id="userService2" factory-bean="userServiceFactory"
factory-method="newUserService2" />

UserServiceFactoryMethod.java

public class UserServiceFactoryMethod{
	public UserService newUserSErvice1() {
		return new UserService();
	}
	public UserService newUserService2() {
		return new UserService();
	}
}

3.2 Bean命名

配置一个Bean时,可以使用 id 或者 name 属性给bean命名。 id 和 name 属性作用上一样,推荐使用id。

  1. id的命名必须满足xml命名规范, id是唯一的, 配置文件中不允许同时存在两个相同id
  2. 同一个bean可以有多个name
  3. 注解和配置文件都存在的时候
    如果配置基本类的时候,注解和配置文件都使用的时候,注解和配置文件中 name 不相同的时候, 则两个不冲突,都能够生效。
    如果配置基本类的时候,注解和配置文件都使用的时候,注解和配置文件中 name 相同的时候,则两个冲突,配置文件生效。

3.3 Bean的作用域

要修改bean的作用域可以在bean标签中使用scope属性设置:
singleton: 在spring IoC容器中仅存在一个Bean的实例, Bean以单例方式存在, 是bean作用域的默认值
prototype: 每次从容器中调用bean时, 都返回一个新的实例, 以多例方式存在
request: 每次HTTP请求都会创建一个新的Bean, 该作用域仅适用于WebApplicationContext环境
session: 同一个HTTPSession共享一个bean, 不同的session用不同的Bean,仅用于WebApplicationContext环境
application: 限定一个bean的作用域为ServletContext的生命周期, 该作用于仅适用于web的spring WebApplicationContext环境

3.4 bean的生命周期

  1. 创建对象
  2. 设置属性
  3. 设置bean的name属性
  4. 获取关联的BeanFactory
  5. 初始化前的操作
  6. 属性设置后
  7. 自定义初始化方法
  8. 初始化后的操作
  9. 使用Bean的业务方法
  10. DisposableBean接口中的destroy方法
  11. 自定义的销毁方法
    LifeCycleService.java
/**
 * Bean的完整生命周期
 */
public class LifeCycleService implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean {
	// 给LifeCycleService Bean设置属性
	private String name;
	public void setName(String name){
		this.name = name;
		System.out.println("2. 设置属性");
	}
	public String getName(){
		return name;
	}
	// 调用无参数构造方法
	public LifeCycleService(){
		System.out.println("1. 创建对象");
	}
	// 设置有参数构造方法
	public LifeCycleService(String name){
		this.name = name;
	}
	
	/*	要知道这个bean在spring中,id或name是什么
		就要实现BeanNameAware这个接口
		目的就是在spring给这个bean加上id或者name后,回调这个方法
		来告诉我们这个对象, 在bean里面叫什么 */
	@Override
	public void setBeanName(String arg0) {
		System.out.println("3. 设置bean的name属性");
	}
	
	/*	获取bean所关联的beanFactory,就是这个bean是哪个Factory创建的
		通过实现BeanFactoryAware接口
	 	@Param: BeanFactory就是applicationContext的一个具体的类 */
	@Override
	public void setBeanFactory(BeanFactory arg0) throws BeansException {
		System.out.println("4. 获取关联的BeanFactory");
	}
	
	/* 全局的对Bean的增强, 在已经有的功能上做一些扩展
	 * 需要创建一个类实现BeanPostProcessor接口
	 * 里面有两个方法,不是强制我们去做
	 * 而是已经提供了默认的实现
	 * postProcessBeforeInitialization: 对bean做一些前置增强, 在调用原始方法之前, 要做什么事
	 * postProcessAfterInitialization: 调用原始的方法之后, 你要做什么
	 * 这里面涉及到后面AOP
	 * 两个方法只是增强bean, 所以最后还是要返回Bean
	 * 
	 * 请参阅下面MyProcessor.java和applicationContext.xml
	 * 
	 * 注意: 处理器也要注册到spring中去, 到applicationContext.xml中注册
	 * 		 这个bean只需要初始化, 不需要获取, 所以直接注册就好了 */
	
	/*
	 * 属性设置之后执行的方法
	 * 实现InitializingBean接口
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		System.out.println("6. 属性设置后");
	}
	
	/*
	 * 执行自定义的初始化方法
	 * 要在applicationContext.xml中指定初始化方法
	 */
	private void init() {
		System.out.println("7. 自定义初始化方法");
	} 
	
	/*
	 * 接下来该怎么用Bean就怎么用, 用完之后才是销毁步骤
	 * 这里执行生命周期无关的Bean的自定义方法
	 */
	public void run(){
		System.out.println("9. 使用Bean的业务方法");
	}
	
	/*
	 * 用完Bean之后,如果关闭了容器, 就会被回收
	 * 回收之前会调用destroy方法
	 * 实现DisposableBean接口
	 */
	@Override
	public void destroy() throws Exception {
		System.out.println("10. DisposableBean接口中的destroy方法");
	}
	
	/*
	 * 这个调用之后会执行自定义的销毁方法
	 * 要在applicationContext.xml中指定销毁方法
	 */
	public void myDestroy() {
		System.out.println("11. 自定义的销毁方法");
	}
}

MyProcessor.java

public class MyProcessor implements BeanPostProcessor {
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		System.out.println("5. 初始化前的操作");
		return bean;
	}
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		System.out.println("8. 初始化后的操作");
		return bean;
	}
}

applicationContext.xml

<!-- 注册实现了BeanPostProcessor接口的处理器 -->
<bean class="com.ttc.lifeCycle.MyProcessor" />
<!-- 注册自定义的init方法和destroy方法 -->
<bean id="lifeCycleService" class="com.ttc.lifeCycle.LifeCycleService"
init-method="init" destroy-method="myDestroy" >
	<property name="name" value="test"></property>
</bean>

这个LifeCycleService类在被getBean调用的时候就会执行全部完整的一个生命周期方法了

3.5 依赖注入

依赖指的是当前对象在运行过程中需要用到的其它参数, spring可以帮我们完成这个依赖关系的建立, 简单说就是把你需要的参数给你, 而你不用管参数是怎么来的, 已尽可能的解耦。

例: Controller中需要Service对象, spring可以把Service自动丢到Controller中, 不需要关心Service是怎么来的, 用就完了

要完成依赖注入, 必须在依赖的一方(controller)中为被依赖的一方(Service)定义属性, 用来接收注入


com.ttc.pojo.User.java&com.ttc.pojo.UserProducts.java

public class User{
	private String name;
	private int age;
	private String sex;
	private UserProducts userProducts;
	// getter & setter
	public User(String name, int age, String sex, UserProducts userProducts) {
		this.name = name;
		this.age = age;
		this.sex = sex;
		this.userProducts = userProducts;
	}
}
public class UserProducts{
	System.out.println("我的作品");
}
3.5.1 构造器注入

java类是需要有一个带参数的构造方法,对象的属性通过构造方法的参数来获得(构造方法的参数一定匹配, 字段名称一定统一)。

applicationContext.xml

<bean id="user" class="com.ttc.pojo.User">
	<constructor-arg name="name" value="tom" />
	<constructor-arg name="age" value="18" />
	<constructor-arg name="sex" value="" />
	<constructor-arg name="userProducts" ref="userProducts" />
</bean>
<bean id="userProducts" class="com.ttc.pojo.UserProducts" />

引入c的命名空间, 简化书写
xmlns:c=“http://www.springframework.org/schema/c”

3.5.2 setter注入

java类要提供属性的set方法, 通过对属性的set方法传参获得实例化后的对象
applicationContext.xml

<bean id="user" class="com.ttc.pojo.User">
	<property name="name" value="tom" />
	<property name="age" value="18" />
	<property name="sex" value="" />
	<property name="userProducts" ref="userProducts" />
</bean>
<bean id="userProducts" class="com.ttc.pojo.UserProducts" />

引入p的命名空间, 简化书写
xmlns:p=“http://www.springframework.org/schema/p”

4. 注解配置

可以使用注解来定义bean以及配置依赖注入,从而可以不用再使用XML繁琐的配置也可以达到相同的效果。

4.1 注解配置Bean

4.1.1 @Component
  1. 在applicationContext.xml中引入context命名空间

applicationContext.xml

<!-- 头文件中加入context命名空间 -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
		xmlns:context="http://www.springframework.org/schema/context"
		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
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">
</beans>
  1. 开启注解支持(@Autowired等)

applicationContext.xml

<!-- 开启注解支持 --> 
<context:annotation-config/>
  1. 配置包扫描器

applicationContext.xml

<!--配置包扫描器--> 
<context:component-scan base-package="com.ttc"/>
注意: 要使用注解, 必须引用context和aop的jar包
  1. 使用注解定义bean
    @Component: 意思就是把我这个类注册到spring容器中, 作为一个bean存在, 要给一个bean的id, 相当于定义一个bean标签

UserService.java

@Component("userService")
public class UserService {
	public void helloWorld() {
		System.out.println("hello world");
	}
}
4.1.2 @Repository、@Service、@COntroller
除了@Component之外, spring提供了三个基本功能和@Component等效的注解
	@Repository		用于对DAO实现类进行标注
	@Service		用于对Service实现类进行标注
	@Controller		用于对Controller实现类进行标注
这三个注解是为了让标注类本身的用途清晰,Spring在后续版本会对其增强

4.2 自动装配Bean

4.2.1 @Value("")

@Value("") 是将一个简单的值注入到属性中去

public class User{
	@Value("Tom")
	private String name;
	@Value("#{T(Math).PI}")
	private double PI;
	private int age;

	@Value("19")
	public void setAge(int age) {
		this.age = age;
	}
}
4.2.2 @Autowired

@Autowired可以将一个Bean对象自动注入到当前属性中。@Autowired默认是按照类型注入(与Bean的名字无关),如果存在两个相同类型的Bean,自动注入将会失败。
备注:@Autowired用于从容器中找一个类型匹配的Bean注入到属性中
@Autowired注入时也可以针对成员变量或者setter方法。
@Autowired相当于applicationContext.xml中bean标签下的property标签。
UserController.java

@Controller
public class UserController{
	@Autowired
	private UserService userService;
	private UserDao userDao;
	@Autowired
	public void setUserDao(UserDao userDao){
		this.userDao = userDao;
	}
}

@Autowired的required属性,设置一定要找到匹配的Bean,否则会报错。
@Qualifier指定注入Bean的名称,注解Bean必须指定相同名称


对于类型相同的Bean, Autowired自动注入失败的问题得到解决

UserDdao.java & UserService.java

@Repository("uDao")
public class UserDao{
}
@Service
public class UserService{
	@Autowired(required = true) // 默认就为true
	@Qualifier("uDao")
	private UserDao userDao;
}
4.2.3 @Resource

上面的例子中, 本来可以用一个标签解决, 却必须搭配@Autowired和@Qualifire两个标签来解决, 于是在spring的扩展包中增加了@Resource注解
UserController.java

public class UserController {
	// @Autowired
	@Resource
	private UserService userService;
	
	public void doGet() {
		userService.helloWorld();
	}
}

关于@Resource的说明:

  1. @Resource不是spring自带的标签,作用在于如果没有指定任何参数,他将默认用属性名称到IoC容器中去找对应的Bean,如果查找不到,在按照类型去查找
  2. 如果指定了@Resource(name = “userService1”), 将会按照name = userService1的方式去查找, 如果IoC容器中没有这个名字的Bean查找失败
  3. 如果指定了@Resource(type = UserService.class), 将会按照类型去查找
  4. 如果都指定了, 按照name查找

4.3 Bean的作用域

使用注解配置的Bean和在applicationContext.xml中配置的Bean一样, 默认作用域都是singleton。
@Scope注解用于指定Bean的作用域, 值和xml中的配置一样:singleton, prototye, request, session, application

5. applicationContext.xml拆分

5.1 联合使用xml和标签

标签有一定的限制(构造器注入不能传参等), 因此搭配xml和标签一起使用

可以在applicationContext.xml中注册bean, 在代码中使用@Autowired等标签自动注入属性

5.2 拆分applicationContext.xml文件

项目中可能会遇见很多Bean的情况, 一个applicationContext.xml文件可能会导致代码相当臃肿, 这时候我们可以拆分applicationContext.xml文件为几个(applicationController.xml、applicationService.xml、applicationUserDao.xml、applicationOrderDao.xml等)
我们在注applicationContext.xml文件中引入其他配置文件即可:

<import resource="applicationController.xml" />
<import resource="classpath:applicationService.xml" />
<import resource="classpath*:application*Dao.xml" />

二、AOP

1. AOP

AOP是Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

2. 为什么需要AOP

  • 案例分析: 项目开发过程中我们经常遇到一系列通用的需求比如: 权限控制, 日志输出, 事务管理, 数据统计等, 这些看似简单的需求, 在实际开发中却会带来麻烦:

UserDao.java

public class UserDao{
	public void insert() {
		System.out.println("insert 1 sql");
	}
	public void update() {
		System.out.println("update 1 sql");
	}
	public void delete() {
		System.out.println("delete 1 sql");
	}
}

在第一个开发版本中, 已经实现了程序的实际功能, 但是后来发现数据库操作出现了卡顿(可能并发导致…), 这时候我要对这些方法进行时间统计, 并输出日志分析问题

  • 解决办法? 难道要在每个方法执行前获取时间, 执行后再获取时间, 相减得出执行时间输出到日志中吗? 也可以, 如果很多方法都要执行这样的操作呢? 另外一个问题, 已经上线了的项目, 还要拿过来重新改源代码, 重新测试上线很麻烦。所以我们不得不找一个更简单的方法去实现。
  • 问题:
    需求实现了,但是:
  1. 修改了源代码,违反了OCP开闭原则
  2. 大量重复代码
  • 用AOP解决: 要对一些已经存在的方法进行功能扩展,要保证在不修改源代码或改变调用方式的情况下为原本的方法增加新功能
    而由于需要扩展的方法有很多,于是把这些方法称作一个切面(切面就是一系列需要扩展功能的方法的集合)
  • AOP的目的: 将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
  • 应用场景: 日志记录,性能统计,安全控制,事务处理,异常处理等等。

3. AOP相关黑话(术语)

  • 连接点(Joinpoint):
    扩展内容与原有内容交互的点,可以理解为要被扩展的地方,也是被拦截的点。通常是一个方法,而AspectJ中也支持属性作为连接点。(上面例子中的连接点为insert、update、delete三个方法)
  • 切点(pointcut):
    真正被扩展的方法称为切点(insert、update、delete三个方法),连接点是要被扩展的地方,切点是真正被扩展的地方。
  • 通知/增强(advice):
    要在切点上增加的功能。
    按照执行时机不同被分为:前置、后置、异常、最终、环绕、引介
    引介通知是指在不修改类代码的前提下,为类增加方法或属性
  • 目标(target):
    目标就是要被增强的对象
    示例中,目标就是UserDao
  • 织入(weaving):
    织入是一个动词,描述的是将扩展功能应用到target的过程。
  • 代理(proxy):
    spring使用代理来完成AOP,对某个对象增强后就得到一个代理对象;

spring AOP的整个过程就是对target应用advice最后产生proxy对象的过程;


用了AOP后,相当于在原对象和调用者之间新增了一个代理对象,在使用过程中,本来我是直接找一个对象执行对象中的方法,现在我先找到代理类,在处理原本类中的逻辑代码。

  • 切面(aspect):
    切面是切入点和通知的结合,是一个抽象概念;
    一个切面是指所有应用了同一个通知的切入点的集合。
    示例中的insert、update、delete方法共同组成了一个切面。

4. AOP的传统实现

4.1 JDK动态代理(官方)

JDK通过字节码的形式,通过接口动态创建一个代理类称为JDK动态代理。
(与之相对的是静态代理,手动新建一个类,该类实现一个接口,并将该接口作为该类的属性,重写每个方法,做相应增强的操作后,调用接口对应的方法,创建的时候直接给这个对象一个接口的实现类作为参数即可。)
动态代理实现类:
UserDao&UserDaoImpl

public interface UserDao {
	public void insert();
	public void delete();
	public void update();
}
// ========================
public class UserDaoImpl implements UserDao {
	public void insert() {
		System.out.println("insert user complete!");
	}
	public void delete() {
		System.out.println("delete user complete!");
	}
	public void update() {
		System.out.println("update user complete!");
	}
}

自定义动态代理实现类

public class MyProxy<T> implements InvocationHandler {
	// 传入我要代理的对象
	//	private UserDaoImpl userDao;
	private T source;
	// 获取一个代理对象
	//	public Object getProxy(UserDaoImpl userDao) {
	public Object getProxy(T source) {
		//	this.userDao = userDao;
		this.source = source;
		/*
		 * @Param: Loader		当前类的类加载器
		 * @Param: Interfaces	代理对象实现的接口(要实现一个类的代理,这个类必须得实现接口)
		 * @Param: @NotNull InvocationHandler	要进行代理的类,如果用自己来处理,必须
		 * 										实现InvocationHandler接口
		 */
		/*Object proxy= Proxy.newProxyInstance(userDao.getClass().getClassLoader(),
				userDao.getClass().getInterfaces(),
				this);*/
		Object proxy = Proxy.newProxyInstance(source.getClass().getClassLoader(),
				source.getClass().getInterfaces(),
				this);
		// 返回接口类型
		return proxy;
	}
	/*
	 * 继承InvocationHandler之后需要实现invoke(执行)方法
	 * 当被调用该代理对象的某个方法的时候,就会自动的调用invoke这个方法,告诉我们有人调用这个方法了
	 * 你要做什么Advice,就可以在这里实现了,完成之后就会调用原始对象,完成操作
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("代理对象的方法被调用了。。。");
		
		/* 
		 * 无论实现什么新的功能,必须调用原始对象的方法
		 * @Param: Object		原始对象
		 * @Param: Object..args	原始方法中的参数
		 */
		//	Object result = method.invoke(userDao, args);
		Object result = method.invoke(source, args);
		
		return result;
	}
}
  • 说明:
  1. 要实现动态代理,首先要代理的类应该有一个接口,才能实现动态代理,基于jdk的底层原理;
  2. 动态代理的实现类首先应该实现InvocationHandler接口,并实现该接口的invoke(执行)方法;
  3. 将要代理的对象作为属性,在获取代理对象方法(getProxy())中实例化,通过代理类的newProxyInstance(ClassLoader, Interfaces, Class)方法获取代理对象

参数说明:

  • ClassLoader:
    因为创建代理类有新对象(基于被代理对象)的产生,要进行实例化要先确定被代理的对象是否进行了实例化,所以需要ClassLoader
  • Interfaces:
    代理对象创建新对象相当于实现了所有被代理对象实现过的方法,即创建了一个与被代理对象有相同实现的子类
  • Class:
    一个实现了InvocationHandler接口的代理类,用来处理要代理的类(要增强的功能),多用当前对象this代理
  1. 实现invoke(Object proxy, Method method, Object[] args)方法,该方法自动被调用,传统的AOP的实现就在这里了;
    在这里可以写通知/增强(Advice)的功能。无论新增什么功能后都要调用原始对象的方法,通过method.invoke(source, args)调用原始对象的方法。

参数说明:
invoke(Object proxy, Method method, Object[] args)

  • proxy:当前代理对象
  • method:原始对象的方法
  • args:原始对象方法参数列表

method.invoke(Object object, Object… args)

  • object:原始对象
  • args:原始对象方法的参数列表

4.2 CGLib(民间)

动态代理的弊端:

  1. 动态代理要求被代理的target对象必须实现了某个接口,且仅能代理接口中声明了的方法

导入CGLib.jar

动态代理实现类

UserDao.java:

public class UserDaoImpl implements UserDao {
	public void insert() {
		System.out.println("insert user complete!");
	}
	public void delete() {
		System.out.println("delete user complete!");
	}
	public void update() {
		System.out.println("update user complete!");
	}
}

自定义CGLibProxy代理类:

public class CGLibProxy<T> implements MethodInterceptor {
	private T source;
	
	// 获取动态代理对象
	public Object getProxy(T source) {
		
		this.source = source;
		
		Enhancer enhancer = new Enhancer();
		
		// 只要有人调用了代理类,就创建一个target的子类
		enhancer.setSuperclass(source.getClass());
		// 如果有人调用了这个代理对象,要提供一个方法处理这个事情
		enhancer.setCallback(this);
		return enhancer.create();
	}
	// 只要我们调用了代理对象就会执行这个方法
	public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
		System.out.println("执行原始方法前的操作!");
		// 调用原始方法
		Object result = method.invoke(source, args);
		
		System.out.println("执行原始方法后的操作!");
		return result;
	}
}
  • intercept方法参数说明:
  1. proxy:CGLib根据指定父类生成的代理对象
  2. method:拦截的方法
  3. args:拦截方法的参数数组
  4. methodProxy:方法的代理对象,用于执行父类的方法
  • 使用CGLib动态代理与JDK动态代理的异同:
  1. CGLib可以不用接口,直接创建一个target的子类
  2. 都需要获取代理对象,自动调用实现方法完成target的增强功能
  3. CGLib不能将target对象以及要被增强的方法用final修饰
  4. 都不能在代理类中循环调用共同方法

5. spring中的传统AOP

spring在运行期间,可以自动生成代理对象,不需要特殊的编译器,springAOP的底层就是通过JDK的动态代理和CGLib动态代理技术,为目标Bean横向织入。并且spring会自动选择代理方式

  1. 若目标对象实现了若干接口,spring使用JDK的java.lang.reflect.Proxy代理
  2. 若目标对象没有实现任何接口,spring使用CGLib库生成目标对象的子类。

程序中应优先对接口创建代理,便于程序解耦。spring只支持方法连接点,不提供属性连接点。

5.1 spring通知/增强 类型

Spring按照通知Advice在目标类方法的连接点位置,通过实现的接口,可以分为下面5种:

  • 前置org.springframework.aop.MethodBeforeAdvice用于在原始方法执行前的预处理
  • 后置org.springframework.aop.AfterReturningAdvice用于在原始方法执行之后的处理
  • 环绕org.aopalliance.intercept.MethodInterceptor在目标方法执行前后实施增强(这里算是拦截器,在这里可以阻止原始方法的执行,而其他通知做不到)
  • 异常org.springframework.aop.ThrowAdvice用于在原始方法抛出异常后实施处理
  • 引介org.springframework.aop.IntroductionInterceptor在目标类中添加一些新的方法和属性(一般不用)

代理对象指定为:
org.springframework.aop.framework.ProxyFactoryBean

5.2 spring AOP切面类型

普通切面(Advisor): 未指定具体切入点的切面,把所有目标全部增强。
切点切面(PointcutAdvisor): 指定具体切点的切面,可以指定拦截目标那些方法
引介切面(IntroductionAdvisor): 引介切面,针对引介通知而使用的切面(不常用)


定义接口:

public interface UserDao {
	public void insert();
	public void delete();
	public void update();
	public void select();
}

定义实现类:

public class UserDaoImpl implements UserDao {
	public void insert() {
		System.out.println("insert Compelete!");
	}
	public void delete() {
		System.out.println("delete Compelete!");
	}
	public void update() {
		System.out.println("update Compelete!");
	}
	public void select() {
		System.out.println("select Compelete!");
	}
}

通知类:

public class MyAdvice implements MethodBeforeAdvice {
	public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
		System.out.println("前置增强。。。");
	}
}
  • 参数说明:
  1. arg0:方法对象
  2. arg1:参数列表
  3. arg2:目标对象
public class MyAdvice implements AfterReturningAdvice {
	public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
		System.out.println("后置增强。。。");
	}
}
  • 参数说明:
  1. arg0:原始方法返回值类型
  2. arg1:方法对象
  3. arg2:参数列表
  4. arg3:目标对象

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {
	@Autowired
	@Qualifier("userDaoProxy")
	private UserDao userDao;
	@Test
	public void test() {
		userDao.insert();
		userDao.delete();
		userDao.update();
		userDao.select();
	}
}
5.2.1 普通切面(Advisor)

applicationContext.xml:

<!-- 为方便理解,还是用手动注册Bean -->
<!-- 目标对象 -->
<bean id="userDao" class="com.ttc.aop.UserDaoImpl" />

<!-- 通知 要增强的具体类 -->
<bean id="beforeAdvice" class="com.ttc.aop.MyAdvice" />

<!-- 代理对象 -->
<bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean" >
	<property name="target" ref="userDao" />
	<property name="interfaces" value="com.ttc.aop.UserDao" />
	<property name="interceptorNames" value="beforeAdvice" />
</bean>

如使用CGLib,在userDaoProxy的Bean中指定:
< property name=“optimize” value=“true” />该项会强制使用CGLib
< property name=“proxyTargetClass” value=“true” />是否是一个普通类(没有接口),如果是普通类将也会使用CGLib
并去掉name=“interfaces” 的参数即可

5.2.2 切点切面(PointcutAdvicor)

通过正则表达式指定切点切面:
定义切点Bean:

  1. 正则表达式class:

org.springframework.aop.support.RegexpMethodPointcutAdvisor

<!-- 目标对象 -->
<bean id="userDao" class="com.ttc.pointcut.UserDaoImpl" />

<!-- 通知 要增强的具体类 -->
<bean id="beforeAdvice" class="com.ttc.pointcut.MyAdvice" />

<!-- 通过正则表达式定义切点 -->
<bean id="pointcutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
	<!-- 定义的正则表达式 -->
	<property name="pattern" value=".*" />
	<!-- 通知 -->
	<property name="advice" ref="beforeAdvice" />
</bean>

<!-- 代理对象 -->
<bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean" >
	<property name="target" ref="userDao" />
	<property name="interfaces" value="com.ttc.pointcut.UserDao" />
	<property name="interceptorNames" value="pointcutAdvisor" />
</bean>
  1. 通过名字的class:

org.springframework.aop.support.DefaultPointcutAdvisor
以及
org.springframework.aop.support.NameMatchMethodPointcut

<!-- 目标对象 -->
<bean id="userDao" class="com.ttc.pointcut.UserDaoImpl" />
<!-- 通知 要增强的具体类 -->
<bean id="beforeAdvice" class="com.ttc.pointcut.MyAdvice" />
<!-- 切点对象 -->
<bean id="pointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
	<property name="mappedNames">
		<list>
			<value>insert</value>
			<value>delete</value>
			<value>update</value>
		</list>
	</property>
</bean>
<!-- 通过名字定义切面 -->
<bean id="pointcutAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
	<property name="advice" ref="beforeAdvice" />
	<property name="pointcut" ref="pointcut" />
</bean>
<!-- 代理对象 -->
<bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="target" ref="userDao" />
	<property name="interfaces" value="com.ttc.pointcut.UserDao" />
	<property name="interceptorNames" value="pointcutAdvisor" />
</bean>

5.3 自动创建代理生成器

前面的案例中,每个代理都是通过ProxyFactoryBean织入切面代理,在实际开发中,非常多的Bean每个都配置ProxyFactoryBean开发维护量巨大。
解决方案——自动创建代理:

  1. 根据Bean名称创建代理:

org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator

<!-- 目标对象 -->
<bean id="userDao" class="com.ttc.autoproxy.UserDaoImpl" />

<!-- 通知 要增强的具体类 -->
<bean id="beforeAdvice" class="com.ttc.autoproxy.MyAdvice" />

<bean id="pointcutAdvisor"
	class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
	<!-- 定义的正则表达式 -->
	<property name="pattern" value=".*update*" />
	<!-- 通知 -->
	<property name="advice" ref="beforeAdvice" />
</bean>

<!-- 自动代理 -->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
	<property name="interceptorNames" value="pointcutAdvisor" />
	<property name="beanNames" value="userDao" />
</bean>
  1. 基于切点信息生成代理:

org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator

<!-- 目标对象 -->
<bean id="userDao" class="com.ttc.autoproxy.UserDaoImpl" />

<!-- 通知 要增强的具体类 -->
<bean id="beforeAdvice" class="com.ttc.autoproxy.MyAdvice" />

<bean id="pointcutAdvisor"
	class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
	<!-- 定义的正则表达式 -->
	<property name="pattern" value=".*update*" />
	<!-- 通知 -->
	<property name="advice" ref="beforeAdvice" />
</bean>

<!-- 自动代理 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />

先找切面信息,再通过切面信息找到目标对象,通过方法名称找到具体信息。通过这种方式,我们只需要:

  • 配置advice和切面信息
  • 定义创建默认切面代理(DefaultAdvisorAutoProxyCreator)

自动代理的特点:

  • 不需要创建代理对象了,直接@Autowired注入就可以了
  • 调用的时候找的已经是代理对象了,而不是原始对象

6. AspectJ实现AOP

6.1 AspectJ简介

AspectJ是一个基于Java语言的AOP框架,Spring2.0以后新增了对AspectJ切点表达式支持。
新版Spring框架建议我们都使用AspectJ方式来开发AOP,并提供了非常灵活且强大的切点表达式,通过JDK5注解技术,允许直接在Bean类中定义切面。

6.2 使用AspectJ

  • 导入Jar包

使用AspectJ 需要导入Spring AOP和 AspectJ相关jar包:

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
	<version>5.2.2.RELEASE</version>
</dependency>
  • 通知类型:
  1. @Before 前置通知
  2. @AfterReturning 后置通知
  3. @Around 环绕通知 彻底拦截原始方法的执行,执行前后都可以增加逻辑,也可以不执行原始方法
  4. @AfterThrowing 抛出通知 执行原始方法出现异常时执行
  5. @After 最终final通知,不管是否异常,原始方法调用后都会执行

6.3 定义切点

在通知中通过上述注解的value属性定义切点,在value属性中可以通过execution函数,可以定义切点的方法切入。

  • 语法:
    execution(访问修饰符 返回类型 方法名 (参数)), throwing = “e”

例:

匹配所有类public方法:execution(public * *(..))
匹配指定包下所有类方法,不包含子包:execution(* com.ttc.dao.*(..))
匹配指定包下所有类方法,包含子包:execution(* com.ttc.dao..*(..))
匹配指定类所有方法:execution(* com.ttc.service.UserService.*(..))
匹配实现特定接口所有类方法 :execution(* com.ttc.dao.UserDao+.*(..))
匹配所有insert开头的方法: execution(* insert*(..))

applicationContext.xml

新增
xmlns:aop="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

<!-- 启用自动代理 -->
<aop:aspectj-autoproxy/>

UserDao.java

@Repository
public class UserDao {
	public void insert() {
		System.out.println("insert Compelete!");
	}
	public void delete() {
		System.out.println("delete Compelete!");
	}
	public void update() {
		System.out.println("update Compelete!");
	}
	public void select() {
		System.out.println("select Compelete!");
	}
}

MyAdvice.java

  • 在后置通知中通过在@AfterReturning中增加一个ruturning参数传递返回结果
  • 在环绕通知中通过ProceedingJoinPoint参数传递方法继续运行;
  • 在除around通知外,通过JoinPoint参数获取当前切点信息;
  • 在抛出中还要在@AfterThrowing中通过一个参数throwing传递异常
@Aspect
@Component
public class MyAdvice {
	@Before("execution(* com.ttc.dao.UserDao.*(..))")
	public void beforeExec() {
		System.out.println("前置增强。。。");
	}
	
	@AfterReturning(value = "execution(* com.ttc.dao.UserDao.select(..))", returning = "result")
	public void afterReturnExec(Object result) {
		System.out.println("后置通知。。。返回值:"+result);
	}
	
	@Around(value = "execution(* com.ttc.dao.UserDao.update(..))")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("方法执行前通知。。。");
		Object result = joinPoint.proceed();
		System.out.println("方法执行后通知---");
		return result;
	}

	@AfterThrowing(value = "execution(* com.ttc.dao.UserDao.insert(..))", throwing = "e")
	public void afterThrowing(JoinPoint joinPoint, Throwable e) {
		System.out.println(joinPoint);
		System.out.println("系统发生异常。。。。");
		System.out.println(e.getMessage());
	}
	
	@After(value = "execution(* com.ttc.dao.UserDao.delete(..))")
	public void afterAll(JoinPoint joinPoint) {
		System.out.println("after:" + joinPoint);
		System.out.println("after....");
	}
}
  • @Pointcut为切点命名:
    在每个通知内定义切点,会造成工作量大,不易维护,对于重复的切点,可以使用@Poingcut进行定义切点方法:private void 无参数方法,方法名为切点名;
@Aspect public class MyAspect {
	@Pointcut("execution(* com.ttc.dao.UserDao.*(..))") 
	public void pointCut1() {} 
	@Before("MyAspect.pointCut1()") 
	public void beforeExec(JoinPoint joinPoint) { 
		System.out.println(joinPoint);
		System.out.println("前置处理"); 
	} 
}
  • 当通知多个切点时,可以使用|| 进行连接,也可以使用逻辑运算符:
@Pointcut("execution(* com.kkb.service.ProductService.*(..))"
			+ "||execution(* com.kkb.service.HelloService.*(..))") 
public void pointCut1() {}

6.4 使用xml配置切面

如果不适用上面的注解也可以用xml配置

<!--切面定义--> 
<bean id="myAspect" class="com.ttc.dao.MyAspect"/> 
<!--被代理的类--> 
<bean id="productService" class="com.ttc.dao.UserDao"/> 
<aop:config proxy-target-class="true"> 
	<aop:aspect ref="myAspect"> 
		<aop:pointcut id="pointCut1" expression="execution(* com.ttc.dao.UserDao.*(..))"/> 
		<!-- 前置通知 -->
		<aop:before method="beforeExec" pointcut-ref="pointCut1"/> 
		<!--环绕通知--> 
		<aop:around method="around" pointcut-ref="pointCut1"/>
		<!--抛出通知--> 
		<aop:after-throwing method="afterThrowing" pointcut-ref="pointCut1" throwing="e"/>
		<!--最终通知--> 
		<aop:after method="afterAll" pointcut-ref="pointCut1"/>
	</aop:aspect> 
</aop:config>

入侵式的通知:
入侵式的通知是指通知类必须实现某个接口,好处在于不用再在标签中指定方法名称了,系统会找到调用哪个方法。
注:与上面方式不能同时使用

<aop:advisor advice-ref="" />
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值