Spring基于注解方式装配Bean
基于注解方式装配Bean
Spring从2.0开始引入基于注解的配置方式,并且不断的进行完善。通过注解的方式可以直接在类上定义Bean的信息,非常方便。
@Component注解来对类进行标注,它可以被Spring容器识别,Spring容器将自动将类转换为容器管理的Bean。
//使用注解之前,我们要先导入aop的jar包
//使用@Component注解定义Bean ,和<bean id="user" class="com.cad.domain.User">是等效的。
@Component("user")
public class User {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void say(){
System.out.println(name+":"+age);
}
}
仅仅在类上定义了注解是不够的,Spring提供了组件扫描,来进行对指定包进行扫描,对拥有注解的类进行实例化等操作。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<!--声明context命名空间-->
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
<!--指定xsd约束位置-->
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--组件扫描:Spring容器会扫描这个包里所有类,从类的注解信息中获取Bean的信息-->
<context:component-scan base-package="com.cad.domain"></context:component-scan>
</beans>
我们进行测试一下,看Bean是否被放入容器中
public class Test {
@org.junit.Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
User user=(User) ac.getBean("user");
user.setName("Tizzy");
user.setAge(18);
user.say();
}
}
除了@Component外,Spring提供了三个功能和@Component等效的注解。
它们一般用于web项目,对DAO,service,web层进行注解,所以也称为Bean的衍生注解。
@Repository:对DAO实现类进行注解
@Service:对service实现类进行注解
@Controller:对web层Controller实现类进行注解
之所以提供这三个特殊的注解,是为了让注解类本身的用途清晰化,此外,Spring还赋予了一些特殊的功能。我们在项目开发中应该尽量使用这种形式
基于注解方式注入属性
Spring通过@Autowired注解实现Bean的自动依赖注入,会默认根据Bean的类型进行注入。
我们使用小例子来演示一下。
WEB层
@Controller("useraction")
public class UserAction {
@Autowired //会根据类型自动注入
private UserService userservice;
public void say(){
userservice.say();
}
}
service层,实现UserService接口
@Service("userservice1")
public class UserServiceImpl implements UserService {
public void say() {
System.out.println("service");
}
}
配置文件,配置组件扫描
<context:component-scan base-package="com.cad.example"></context:component-scan>
测试一下
public class Test {
@org.junit.Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
UserAction user=(UserAction) ac.getBean("useraction");
user.say();
}
}
输出:service。
问题:@Autowired默认按类型匹配的方式,在容器中查找匹配的Bean,当有且只有一个匹配的Bean时,Spring将其注入到@Autowired注解的变量中。但是如果容器中有超过一个以上的匹配Bean时,例如有两个UserService类型的Bean,这时就不知道将哪个Bean注入到变量中,就会出现异常。
例如上面例子,我们再创建一个UserService接口的实现类
@Service("userservice2")
public class otherUserService implements UserService {
public void say() {
System.out.println("service222");
}
}
这时我们再运行就会出现异常,为了解决这个问题,Spring可以通过@Qualifier注解来注入指定Bean的名称。
@Controller("useraction")
public class UserAction {
@Autowired
//指定指定Bean的名称
@Qualifier("userservice2")
private UserService userservice;
public void say(){
userservice.say();
}
}
这时候otherUserService就被注入
输出结果service222
@Autowired可以对类成员变量的set方进行注解。
@Service("userservice1")
public class UserServiceImpl implements UserService {
private UserDao userdao;
//对set方法使用注解,UserDao的实例就会被注入进来
@Autowired
public void setUserdao(UserDao userdao){
this.userdao=userdao;
}
public void say() {
userdao.add();
}
}
dao层
@Repository
public class UserDao {
public void add(){
System.out.println("dao add.....");
}
}
注解方式配置Bean的作用范围和生命过程方法
通过注解配置的Bean和通过< bean >配置的Bean一样,默认的作用范围都是singleton,Spring为注解配置提供了一个@Scope的注解,显式指定Bean的作用范围。
@Controller("user")
//指定作用范围为多例prototype
@Scope("prototype")
public class User {
public void say(){
System.out.println("hello.word");
}
}
Spring定义的@PostConstruct和@PreDestroy两个注解相当于bean的init-method和destory-method属性的功能
@Controller("user")
@Scope("prototype")
public class User {
public void say(){
System.out.println("hello.word");
}
@PostConstruct
public void myinit(){
System.out.println("初始化....");
}
@PreDestroy
public void mydestory(){
System.out.println("销毁中....");
}
}
整合多个Spring配置文件
对于一个大型项目而言,可能有多个XML配置文件,在启动Spring容器时,可以通过一个String数组指定这些配置文件。Spring还允许我们通过< import >标签将多个配置文件引入到一个文件中,进行配置文件的集成,这样启动Spring容器时,就仅需指定这个合并好的配置文件即可。
- 第一种方式,使用String数组指定所有配置文件
ApplicationContext ac=new ClassPathXmlApplicationContext(new String[]{"bean1.xml","bean2.xml"});
- 第二种方式,使用import标签
//resource属性指定配置文件位置,支持Spring标准的资源路径
<import resource="classthpath:com/cad/domain/bean1.xml"/>
<import resource="classthpath:com/cad/domain/bean2.xml"/>
第一种方式并不容易维护,我们在开发中推荐使用第二种方式。
Spring AOP
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。面向切面编程这个概念一直被很多人诟病,因为它和IoC一样晦涩,不太容易理解。
AOP到底是什么
我们先来看看以前的OOP(面向对象编程)解决的问题。按照软件重构思想的理念,如果多个类中出现了相同的代码,应该考虑定义一个共同的抽象类,将这些代码提取到抽象类中。
例如dog,cat这些动物对象都应该有eat()、run()等功能,所以我们通过引入包含这两个方法抽象的Animal父类,然后dog、cat这些类通过继承复用eat()、run()等功能。我们大多数情况通过引入父类来消除多个类中重复的代码,但有的时候并没这么简单,我们看下面的例子。
public class Dog implements Animal {
public void run() {
System.out.println("主人发出命令");
System.out.println("跑");
System.out.println("主人给予奖励");
}
public void eat() {
System.out.println("主人发出命令");
System.out.println("吃");
System.out.println("主人给予奖励");
}
}
仔细看上面的代码,我们会发现run()和eat()方法中有很大一部分重复代码,这些代码无法完全提取出来,这些情况会发生在很多地方,如性能监测、访问控制、事务管理及日志记录等。
假设我们将Dog类看成一颗圆木,eat()和run()方法堪称圆木上的一截,我们会发现那些重复代码(事务开启和关闭、主人发出命令和给予奖励)就像一圈年轮,真正的业务代码是树心,这就是这些重复代码被称为横切代码概念的由来。
我们无法通过抽象父类的方式消除以上所述的重复性横切代码,因为这些横切代码依附在具体的业务方法中。AOP独辟蹊径通过横向抽取机制为这类无法通过纵向继承的重复性代码提供了解决方案,AOP希望将这些分散在业务逻辑中的相同代码,通过横向切割的方式抽取到一个独立模块中,我们知道将这些重复性的横切代码提取出来是很容易的,但如何将这些独立的模块融合到业务逻辑中完成和以前一样的操作,这才是关键,也是AOP解决的主要问题。
AOP术语
连接点(Joinpoint):程序执行的某个特定位置:如类初始化前,类初始化后,某个方法调用前,方法调用后,方法抛出异常后。一个类或一段代码拥有一些具有边界性质的特定点,就被称为 “连接点”。Spring仅支持方法的连接点,仅能在方法调用前后、方法抛出异常时这些连接点织入增强。
切点(Pointcut):每个程序都拥有很多连接点,如有一个两个方法的类,这两个方法都是连接点。如何定位到某个连接点上呢?AOP通过“切点”定位特定的连接点。例如:数据库记录是连接点,切点就是查询条件,用来定位特定的连接点。在Spring中,切点通过Pointcut接口描述,使用类和方法作为连接点的查询条件,Spring AOP的解析引擎负责解析切点设定的条件,找到对应的连接点。连接点是方法执行前后等具体程序执行点,而切点只定位到某个方法上,所以如果定位到具体的连接点,还需要提供方位信息,即方法执行前还是执行后。
增强/通知(Advice):增强是织入到目标类连接点上的一段程序代码。在Spring中,增强除了描述一段程序代码外,还拥有另一个和连接点相关的信息,就是执行点的方位。结合方位信息和切点信息,就可以找到特定的连接点。Spring提供的增强接口都是带方位名的:BeforeAdvice,AfterRetuningAdvice,ThrowsAdvice等。所以只有结合切点和增强,才能确定特定的连接点并织入增强代码。
目标对象(Target):需要织入增强代码的目标类。
引介(Introduction):引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使某个类没有实现某个接口,通过AOP的引介功能,我们可以动态的为该类添加接口的实现逻辑,让该类成为这个接口的实现类。
织入(Weaving):织入是将增强添加到目标类上具体连接点的过程。
代理(Proxy):一个类被AOP织入增强后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。
切面(Aspect):切面由切点和增强组成,既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP就是负责实施切面,将定义的横切逻辑织入到切面指定的连接点中。
AOP基础知识
AOP的工作重心在于如何将增强应用于目标对象的连接点上,包括两个工作。
第一:如何通过切点和增强定位到连接点
第二,如何在增强中编写切面的代码。
Spring AOP使用动态代理技术在运行期织入增强的代码。Spring AOP使用了两种代理机制,一种是基于JDK的动态代理;另一种是基于CGLib的动态代理,之所以使用两种代理机制,很大程度上是因为JDK本身只提供接口的代理,不支持类的代理。
JDK动态代理
我们以前学习过JDK动态代理,这里我们来演示一个前面的例子。
//动物接口
public interface Animal {
public void run();
public void eat();
}
//狗的实现类
public class Dog implements Animal {
public void run() {
System.out.println("跑");
}
public void eat() {
System.out.println("吃");
}
}
//现在我们需要在run()和eat()方法前后加一些动作,这些动作是相同的,所以我们提取出来
public class MyAspect {
public void before(){
System.out.println("主人发出命令");
}
public void after(){
System.out.println("主人给予奖励");
}
}
//现在我们需要使用动态代理将增强逻辑和方法编织在一起
public class MyProxyFactory {
public static Animal createDog(){
final Animal dog=new Dog();
final MyAspect aspect=new MyAspect();
//生成代理对象
Animal dogproxy=(Animal) Proxy.newProxyInstance(MyProxyFactory.class.getClassLoader(), dog.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] arg2) throws Throwable {
aspect.before();
Object obj=method.invoke(dog, arg2);
aspect.after();
return obj;
}
});
return dogproxy;
}
}
//我们来进行测试
public class TestJDK {
@Test
public void test(){
Animal animal=MyProxyFactory.createDog();
animal.eat();
}
}
我们发现程序的运行结果和直接在方法里面编写重复代码的效果是一样的,但是重复代码已经被我们提取到了某个类中。
CGLib动态代理
我们以前讲过,使用JDK动态代理有一个很大的限制,就是只能为接口创建代理实例。
对于没有通过接口定义的类,如何动态创建代理实例呢?CGLib填补了这么空缺。
CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并织入横切逻辑。但是由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中的private、final等方法进行代理
CGLib使用需要导入cglib.jar核心包和asm.jar依赖包,但是Spring 的Core.jar核心包中已经包含了这两个jar包,所以我们直接使用即可。
我们还使用上面的小例子,不过要稍作一些修改。
//我们还实现一个Dog类,但是不需要实现任何接口
public class Dog {
public void run() {
System.out.println("跑");
}
public void eat() {
System.out.println("吃");
}
}
//把横切逻辑代码提取出来的类
public class MyAspect {
public void before(){
System.out.println("主人发出命令");
}
public void after(){
System.out.println("主人给予奖励");
}
}
//我们使用CGLib生成代理对象
public class CglibProxy {
public static Dog createDog(){
//CGLib核心类
Enhancer enhancer=new Enhancer();
Dog dog=new Dog();
MyAspect aspect=new MyAspect();
//设置父类,因为CGLib底层是生成需要被代理类的子类
enhancer.setSuperclass(dog.getClass());
//设置回调方法,和InvocationHandler起到的效果一样
enhancer.setCallback(new org.springframework.cglib.proxy.MethodInterceptor() {
//intercept方法会拦截所有目标类方法的调用
//proxy表示目标类的实例,method为目标类方法的反射对象,arg2是方法的参数,arg3是代理类实例
@Override
public Object intercept(Object proxy, Method method, Object[] arg2, MethodProxy arg3) throws Throwable {
aspect.before();
Object obj=method.invoke(dog, arg2);
aspect.after();
return obj;
}
});
//创建代理对象
Dog dogproxy=(Dog) enhancer.create();
return dogproxy;
}
}
//我们进行测试一下
public class TestCGLib {
@Test
public void test(){
Dog dog=CglibProxy.createDog();
dog.run();
}
}
代理出现的一些问题
Spring AOP的底层就是通过使用JDK动态代理或者CGLib动态代理技术为目标Bean织入横切逻辑。
我们虽然通过JDK动态代理和CGLib完成了横切逻辑的动态织入,但我们的方式存在很多需要改进的地方:
- 目标类的所有方法都添加了横切逻辑,而有时,我们只想对某些方法添加横切逻辑。
- 我们通过硬编码的方式指定了织入横切逻辑的连接点,显然不利于修改和维护。
- 我们纯手工的编写创建代理的过程,过程繁琐,为不同类创建代理时,需要分别编写不同的代码,无法做到通用。
Spring AOP为我们完全解决了上述的问题,Spring AOP通过切点指定在哪些类的哪些方法上织入横切逻辑,通过增强描述横切逻辑代码和方法的具体织入点。Spring通过切面将切点和增强组装起来,通过切面的信息,就可以利用JDK或者CGLib的动态代理技术采用统一通用的方式为Bean创建代理对象。
Spring中增强类型
Spring使用增强类定义横切逻辑,同时由于Spring只支持方法连接点,增强还包括了在方法的哪一点织入横切代码的方位信息,所以增强既包括横切逻辑,还包括连接点的部分信息。
AOP联盟为增强定义了Advice接口,Spring支持五种类型的增强。
前置增强:org.springframework.aop.BeforeAdvice 代表前置增强,因为Spring目前只支持方法级的增强,所以MethodBeforeAdvice是目前可用的前置增强,表示在目标方法执行前实施置增强,而BeforeAdvice是为了将来扩展而定义的。
后置增强:org.springframework.aop.AfterReturningAdvice 代表后置增强,表示在目标方法执行后实施增强。
环绕增强:org.aopalliance.intercept.MethodInterceptor 代表环绕增强,表示在目标方法的执行前后实施增强。
异常抛出增强:org.springframework.aop.ThrowsAdvice 代表抛出异常增强,表示在目标方法抛出异常后实施增强。
引介增强:org.springframework.aop.IntroductionInterceptor 代表引介增强,表示在目标类中添加一些新的方法和属性。
这些增强接口都有一些方法,通过实现这些接口的方法,在接口方法中定义横切逻辑,就可以将他们织入到目标类方法的特定连接点
Spring中前置增强
我们还使用我们的狗的例子,即简单又生动。我们的狗只有很单一的功能,我们需要在功能执行前增加一些功能。
//定义动物接口
public interface Animal {
public void run();
public void eat();
}
//狗的实现类
public class Dog implements Animal{
public void run() {
System.out.println("跑");
}
public void eat() {
System.out.println("吃");
}
}
//我们使实现方法前置增强接口,并写入我们的横切逻辑代码
public class MyBeforeAdvice implements MethodBeforeAdvice {
//method为目标类的方法,args为目标类方法的所需参数,obj为目标类实例
public void before(Method method, Object[] args, Object obj) throws Throwable {
System.out.println("主人发出命令");
}
}
//我们测试一下
public class Test {
@Test
public void test(){
Animal dog=new Dog();
BeforeAdvice advice=new MyBeforeAdvice();
//Spring提供的代理工厂
ProxyFactory pf=new ProxyFactory();
//设置代理目标
pf.setTarget(dog);
//为代理目标添加增强
pf.addAdvice(advice);
//生成代理对象
Animal proxy=(Animal)pf.getProxy();
proxy.run();
}
}
解析ProxyFactory
在上面的测试中,我们使用ProxyFactory代理工厂将增强Advice织入到目标类Dog中,ProxyFactory内部就是使用JDK动态代理或者CGLib动态代理技术。
Spring中定义了AopProxy接口,并且提供了两个final类型的实现类Cglib2AopProxy和JdkDynamicAopProxy。
Cglib2AopProxy使用CGLib技术创建代理,JdkDynamicAopProxy通过JDK动态代理技术创建代理。
如果通过ProxyFactory的setInterfaces(Classs[] interfaces)方法指定针对接口进行代理,ProxyFactory就使用JdkDynamicAopProxy,如果是针对类的代理,则使用Cglib2AopProxy。此外还可以通过ProxyFactory的setOptimize(true)方法,让ProxyFactory启动优化处理方式,这样,针对接口的代理也会使用Cglib2AopProxy。
使用配置文件声明代理
虽然通过ProxyFactory已经简化了很多操作,但是Spring给我们提供了通过配置文件来声明一个代理来让免去硬编码
<!--实例化目标对象-->
<bean id="target" class="com.cad.demo.Dog"></bean>
<!--实例化增强对象-->
<bean id="advice" class="com.cad.demo.MyBeforeAdvice"></bean>
<!--实例化代理对象-->
<bean id="proxydog" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--指定代理的接口,有多个的话可以使用数组、list等标签-->
<property name="proxyInterfaces" value="com.cad.demo.Animal"></property>
<!--指定使用的增强-->
<property name="interceptorNames" value="advice"></property>
<!--指定代理的目标类-->
<property name="target" ref="target"></property>
</bean>
ProxyFactoryBean是FactoryBean接口的实现类,我们前一节中介绍了FactoryBean的功能,它负责实例化一个Bean。ProxyFactoryBean负责为其他Bean创建代理对象,内部使用ProxyFactory来完成。我们来了解一下可配置的属性
target:指定要代理的目标对象。
proxyInterfaces:代理要实现的接口,可以是多个接口,使用数组、list标签来指定。
interceptorNames:需要织入的增强类。 是String[]类型,接受增强Bean的id而不是实例。
singleton:返回的代理是否是单实例,默认是单实例。
optimize:当设置为true时,强制使用CGLib代理。
proxyTargetClass:是否对类进行代理,设置为true,使用CGLib代理。
//我们测试一下
public class Test {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
Animal dogproxy=(Animal) ac.getBean("proxydog");
dogproxy.eat();
}
}
后置增强和前置增强一模一样,不需要过多说明。
Spring中环绕增强
环绕增强允许在目标方法调用前后织入增强,综合实现了前置、后置增强两者的功能。
我们还是使用上面小狗的例子。
//创建环绕增强实现类
public class MyInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("主人发出命令");
//使用proceed()方法反射调用目标类相应的方法
Object obj=invocation.proceed();
System.out.println("主人给予奖励");
return obj;
}
}
<!--修改配置文件 -->
<bean id="target" class="com.cad.demo.Dog"></bean>
<bean id="advice" class="com.cad.demo.MyInterceptor"></bean>
<bean id="proxydog" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.cad.demo.Animal"></property>
<property name="interceptorNames" value="advice"></property>
<property name="target" ref="target"></property>
</bean>
//我们测试一下
public class Test {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
Animal dogproxy=(Animal) ac.getBean("proxydog");
dogproxy.eat();
}
}
Spring中异常抛出增强
异常抛出增强最适合的应用场景是事务管理,当执行事务发生异常时,就必须回滚事务。
我们给出一个模拟的例子,来说明异常抛出增强的使用。
//模拟业务类中的转账操作,抛出异常
public class UserService {
public void addMoney(){
throw new RuntimeException("转账异常");
}
}
//我们实现异常抛出增强接口,对转账业务方法进行增强处理
public class TransactionManager implements ThrowsAdvice {
public void afterThrowing(Method method,Object[] args,Object target,Exception e)throws Throwable{
System.out.println(method.getName());
System.out.println(e.getMessage());
System.out.println("回滚事务");
}
}
ThrowsAdvice异常抛出增强接口没有定义任何方法,它是一个标识接口,在运行期Spring使用反射机制自行判断,我们必须使用以下签名形式定义异常抛出的增强方法。
- void afterThrowing(Method method,Object[] args,Object target,Throwable)
方法名必须为afterThrowing,前三个参数是可选的,最后一个参数是Throwable或者其子类。可以在同一个增强中定义多个afterThrowing()方法,当目标类方法抛出异常时,Spring会自动选用最匹配的增强方法,主要是根据异常类的匹配程度。
<!--编写配置文件-->
<bean id="target" class="com.cad.demo.UserService"></bean>
<bean id="advice" class="com.cad.demo.TransactionManager"></bean>
<bean id="proxyservice" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="advice"></property>
<property name="target" ref="target"></property>
<!--因为我们的业务类没有实现接口,因此使用CGLib代理-->
<property name="proxyTargetClass" value="true"></property>
</bean>
//我们进行测试一下
public class Test {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
UserService service=(UserService) ac.getBean("proxyservice");
service.addMoney();
}
}
Spring中切点类型
在学习增强时,我们可能注意到一个问题:增强被织入到目标类的所有方法中。我们希望有选择的织入到某些特定的方法中,就需要使用切点来进行目标连接点的定位。
Spring通过Pointcut接口描述切点,Pointcut由ClassFilter和MethodMatcher构成,通过ClassFilter定位到某些特定类上,通过MethodMatcher定位到某些特定方法上,这样Pointcut就拥有了描述某些类的特定方法的能力。
Spring支持两种方法匹配器:静态方法匹配器和动态方法匹配器。静态方法匹配器仅对方法名签名(包括方法名和入参类型和顺序)进行匹配。动态方法匹配器会在运行期检查方法入参的值。静态匹配仅匹配一次,而动态匹配因为每次调用方法的参数都可能不一样,所以每次调用都需要判断。所以动态匹配影响性能,不常使用。
切点类型
静态方法切点::org.springframework.aop.support.StaticMethodMatcherPointcut是静态方法切点的抽象基类,默认情况下它匹配所有的类。StaticMethodMatcherPointcut包括两个主要的子类,分别是NameMatchMethodPointcut和AbstractRegexpMethodPointcut,前者提供简单字符串匹配方法签名,而后者使用正则表达式匹配方法签名。
动态方法切点:org.springframework.aop.support.DynamicMethodMatcherPointcut 是动态方法切点的抽象基类,默认情况下它匹配所有的类。DynamicMethodMatcherPointcut类已经过时,可以使用DefaultPointcutAdvisor 和DynamicMethodMatcherPointcut动态方法匹配器替代之。
注解切点:org.springframework.aop.support.AnnotationMatchingPointcut实现类表示注解切点。使用AnnotationMatchingPointcut支持在Bean中直接通过JDK5.0注解标签定义的切点。
表达式切点:org.springframework.aop.support.ExpressionPointcut接口主要是为了支持AspectJ切点表达式语法而定义的接口。
流程切点:org.springframework.aop.support.ControlFlowPointcut实现类表示控制流程切点。ControlFlowPointcut是一种特殊的切点,它根据程序执行堆栈的信息查看目标方法是否由某一个方法直接或间接发起调用,以此判断是否为匹配的连接点。
复合切点:org.springframework.aop.suppot.ComposablePointcut实现类是为创建多个切点而提供的方便操作类。它所有的方法都返回ComposablePointcut类,这样,我们就可以使用链接表达式对切点进行操作。
Spring中切面类型
由于增强既包含横切代码,又包含部分的连接点信息,所以我们可以仅通过增强类生成一个切面。 Spring使用org.springframework.aop.Advisor接口表示切面的概念,一个切面同时包含了增强和切点的信息。切面可以分为三类:一般切面、切点切面、引介切面。
Advisor:代表一般切面,它仅包含一个Advice。我们说过,因为Advice包含了横切代码和连接点的部分信息,所以Advice本身就是一个简单的切面,但是它代表的连接点是目标类的所有方法,横切面太宽泛,不会直接使用。
PointcutAdvisor:代表具有切点的切面,它包含Pointcut和Advice两个可u,这样,我们就可以通过类名、方法名、方法方位等信息灵活的定义切面的连接点。
IntroductionAdvisor:代表引介切面
我们主要学习PointcutAdvisor,PointAdvisor主要有六个具体的实现类
DefaultPointcutAdvisor:最常用的切面类型,它可以通过任意Pointcut和Advice定义一个切面,不支持引介切面类型,可以通过扩展该类实现自定义的切面。
NameMatchMethodPointcutAdvisor:通过该类可以定义按方法名定义切点的切面。
RegexpMethodPointcutAdvisor:对于按照正则表达式匹配方法名定义切点的切面,通过该类进行操作。
StaticMethodMatcherPointcutAdvisor:静态方法匹配器切面定义的切面,默认情况匹配所有目标类。
AspectJExpressionPointcutAdvisor:用于AspectJ切点表达式定义切点的切面。
AspectJPointcutAdvisor:用于AspectJ语法定义切点的切面。
演示静态方法匹配切面
StaticMethodMatcherPointcutAdvisor代表一个静态方法匹配切面,通过类过滤和方法名匹配定义切点。
我们创建两个类 ,Dog和Cat
public class Dog {
public void eat(){
System.out.println("狗在吃饭");
}
public void run(){
System.out.println("狗在跑步");
}
}
public class Cat {
public void eat(){
System.out.println("猫在吃饭");
}
public void run(){
System.out.println("猫在跑步");
}
}
//现在我们定义一个切面,在Dog的eat()方法调用前织入一个增强
//StaticMethodMatcherPointcutAdvisor抽象类唯一需要实现的是matches()方法
//但是默认情况,该切面匹配所有类,我们通过覆盖getClassFilter方法,仅匹配Dog及其子类
public class MyAdvisor extends StaticMethodMatcherPointcutAdvisor {
//匹配方法名称为eat的方法
public boolean matches(Method method, Class<?> clazz) {
return method.getName().equals("eat");
}
//我们通过覆盖getClassFilter方法,仅匹配Dog及其子类
public ClassFilter getClassFilter() {
return new ClassFilter(){
public boolean matches(Class<?> clazz) {
return Dog.class.isAssignableFrom(clazz);
}
};
}
}
//我们还需要定义一个增强,我们使用前置增强
public class MyBeforeAdvice implements MethodBeforeAdvice {
public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
System.out.println("主人发出命令");
}
}
<!--配置文件-->
<?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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--Dog目标类和Cat目标类-->
<bean id="targetDog" class="com.cad.spring.Dog"></bean>
<bean id="targetCat" class="com.cad.spring.Cat"></bean>
<!--增强类-->
<bean id="advice" class="com.cad.spring.MyBeforeAdvice"></bean>
<!--切面,里面包含增强,还有一个classFilter属性,可以指定类匹配过滤器,但我们在类里面已经通过覆盖来实现类匹配过滤器,所以不用配置-->
<bean id="MyAdvisor" class="com.cad.spring.MyAdvisor">
<property name="advice" ref="advice"></property>
</bean>
<!--小狗代理-->
<bean id="dogproxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="MyAdvisor"></property>
<property name="proxyTargetClass" value="true"></property>
<property name="target" ref="targetDog"></property>
</bean>
<!--小猫代理-->
<bean id="catproxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="MyAdvisor"></property>
<property name="proxyTargetClass" value="true"></property>
<property name="target" ref="targetCat"></property>
</bean>
</beans>
//我们进行测试一下
public class TestDemo {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
Dog dog=(Dog)ac.getBean("dogproxy");
Cat cat=(Cat)ac.getBean("catproxy");
dog.eat();
System.out.println("-------------------------");
cat.eat();
}
}
可见切面只织入到Dog的eat方法前的连接点上,并且Dog的run()方法没有织入切面。
我们只演示这些,还有动态切面,复合切点切面等都可以自行找相关资料,大概流程都差不多。用的也不多
Spring自动创建代理
在前面所有的例子中,对于每个代理对象,我们都需要进行配置,需要代理的对象很多的时候就会很麻烦。
Spring为我们提供了自动代理机制,让容器为我们自动生成代理,免去我们繁琐的配置功能,内部原理是使用BeanPostProcessor后处理Bean自动地完成这项工作,当我们获取对象时,后处理Bean会获取我们对象的代理,然后最后返回的是我们获取Bean的代理。
这些基于BeanPostProcessor的自动代理创建器的实现类,根据一些规则自动在容器实例化Bean时为匹配的Bean生成代理,这些代理创建器可以分为以下三类。
基于Bean配置名规则的自动代理创建器:允许为一组特定配置名的Bean自动创建代理。实现类为BeanNameAutoProxyCreator
基于Advisor匹配机制的自动代理创建器:它会对容器中所有的Advisor进行扫描,自动将这些切面应用到匹配的Bean中,实现类为DefaultAdvisorAutoProxyCreator
基于Bean中AspectJ注解标签的自动创建代理器:为包含AspectJ注解的Bean自动创建代理。实现类是AnnotationAwareAspectJAutoProxyCreator
演示BeanNameAutoProxyCreator
我们还使用前面猫和狗的例子,不过我们需要修改配置文件
<?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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
//猫和狗的目标类
<bean id="targetDog" class="com.cad.spring.Dog"></bean>
<bean id="targetCat" class="com.cad.spring.Cat"></bean>
//增强类
<bean id="advice" class="com.cad.spring.MyBeforeAdvice"></bean>
//配置BeanNameAutoProxyCreator实现类
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
//beanNames属性允许用户指定需要自动代理的Bean名称,可以使用*通配符,例如*t,就会匹配cat
<property name="beanNames" value="targetDog,targetCat"></property>
<property name="interceptorNames" value="advice"></property>
//指定使用CGLib
<property name="optimize" value="true"></property>
</bean>
</beans>
//我们进行测试
public class TestDemo {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
Dog dog=(Dog)ac.getBean("targetDog");
Cat cat=(Cat)ac.getBean("targetCat");
dog.eat();
System.out.println("-------------------------");
cat.eat();
}
}
AspectJ学习
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
Spring2.0之后增加了对AspectJ切点表达式的支持。@AspectJ是AspectJ1.5新增的功能,通过JDK的注解技术,允许开发者在Bean上直接通过注解定义切面。Spring使用和@AspectJ相同风格的注解,并通过AspectJ提供的注解库和解析库来处理切点。
AspectJ切点表达式
@AspectJ支持三种通配符
* 匹配任意字符,只匹配一个元素
.. 匹配任意字符,可以匹配多个元素 ,在表示类时,必须和*联合使用
+ 表示按照类型匹配指定类的所有类,必须跟在类名后面,如com.cad.Car+,表示继承该类的所有子类包括本身
逻辑运算符
切点表达式由切点函数组成,切点函数之间还可以进行逻辑运算,组成复合切点。
- &&:与操作符。相当于切点的交集运算。xml配置文件中使用切点表达式,&是特殊字符,所以需要转义字符&;来表示。
- ||:或操作符。相当于切点的并集运算。
- !:非操作符,相当于切点的反集运算。
Spring支持9个@AspectJ切点表达式函数,它们用不同的方式描述目标类的连接点。我们来了解几个常用的
-
execution()
execution()是最常用的切点函数,用来匹配方法,语法如下
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
修饰符和异常可以省略。使用例子
-execution(public * *(..)):匹配目标类的所有public方法,第一个*代表返回类型,第二个*代表方法名,..代表方法的参数。
-execution(**User(..)):匹配目标类所有以User为后缀的方法。第一个*代表返回类型,*User代表以User为后缀的方法
-execution(* com.cad.demo.User.*(..)):匹配User类里的所有方法
-execution(* com.cad.demo.User+.*(..)):匹配该类的子类包括该类的所有方法
-execution(* com.cad.*.*(..)):匹配com.cad包下的所有类的所有方法
-execution(* com.cad..*.*(..)):匹配com.cad包下、子孙包下所有类的所有方法
-execution(* addUser(Spring,int)):匹配addUser方法,且第一个参数类型是String,第二个是int args()
该函数接受一个类名,表示目标类方法参数是指定类时(包含子类),则匹配切点。
args(com.cad.User):匹配addUser(User user)方法等-
within()
匹配类,语法
within(<类>)within(com.cad.User):匹配User类下的所有方法
-
target()
target()函数通过判断目标类是否按类型匹配指定类决定连接点是否匹配。
例如 target(com.cad.User):如果目标类类型是User没那么目标类所有方法都匹配切点。
this()
this()函数判断代理对象的类是否按类型匹配指定类。
AspectJ增强类型
Before:前置增强。相当于BeforeAdvice的功能,方法执行前执行。
AfterReturning:后置增强。相当于AfterReturningAdvice,方法执行后执行。
Around:环绕增强。
AfterThrowing:异常抛出增强。
After:不管是抛出异常还是正常退出,该增强都会执行,类似于finally块。
DeclareParents:引介增强。
AspectJ基于XML配置切面
使用AspectJ我们需要的jar包有aopalliance-1.0.jar,aspectjweaver-1.8.10.jar,spring-aop-4.3.8.RELEASE.jar,spring-aspects-4.3.8.RELEASE.jar。
我们先来一个简单的配置,看看怎么使用
//我们先创建Dog类
public class Dog {
public void eat(){
System.out.println("狗在吃饭");
}
public void run(){
System.out.println("狗在跑步");
}
}
//我们创建一个增强类
public class AdviceMethod {
public void before(){
System.out.println("主人发出命令");
}
}
<!--配置文件-->
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" //声明aop命名空间
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop //引入aop xsd文件http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-实例Dog类和增强类-->
<bean id="dog" class="com.cad.aspectj.Dog"></bean>
<bean id="advices" class="com.cad.aspectj.AdviceMethod"></bean>
<!-配置aop,proxy-target-class属性设定为true时,使用CGLib,为false时,使用JDK动态代理-->
<aop:config proxy-target-class="true">
<!--使用<aop:aspect>标签定义切面,ref引入增强-->
<aop:aspect ref="advices">
<!--通过<aop:before>声明一个前置增强,pointcut属性使用切点表达式,method指定使用增强类中方法-->
<aop:before pointcut="execution(* *(..))" method="before" />
</aop:aspect>
</aop:config>
</beans>
//我们测试一下
public class TestDemo {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
Dog dog=(Dog)ac.getBean("dog");
dog.eat();
dog.run();
}
}
配置切点
我们上面的代码中,直接在< aop: before>前置增强标签里使用了表达式来声明切点。
我们还可以在外面配置一个切点,使用的时候直接引用即可。
<aop:config proxy-target-class="true">
<aop:aspect ref="advices">
//定义一个切点
<aop:pointcut expression="execution(* *(..))" id="mypointcut"/>
//直接通过id引用即可
<aop:before pointcut-ref="mypointcut" method="before" />
</aop:aspect>
</aop:config>
<aop:pointcut>元素如果位于 <aop:aspect>元素之中,则只能被当前<aop:aspect>中的元素访问到。为了能被整个 <aop:config>元素中定义的所有切面访问到,必须在<aop:config>下定义。
后置增强
//我们在我们的增强类里添加一个后置增强方法after
public class AdviceMethod {
public void before(){
System.out.println("主人发出命令");
}
public void after(){
System.out.println("主任给予奖励");
}
}
<bean id="dog" class="com.cad.aspectj.Dog"></bean>
<bean id="advices" class="com.cad.aspectj.AdviceMethod"></bean>
<aop:config proxy-target-class="true">
<aop:aspect ref="advices">
<aop:pointcut expression="execution(* *(..))" id="mypointcut"/>
<aop:before pointcut-ref="mypointcut" method="before" />
//配置后置增强
<aop:after-returning pointcut-ref="mypointcut" method="after"/>
</aop:aspect>
</aop:config>
< aop:after-returning >后置增强有一个returning属性,该属性对应后置方法里的参数值,并且必须与方法里的参数值名称相同,该参数用来接收目标方法执行后的返回值。
//我们run()方法返回一个String字符串
public class Dog {
public void eat(){
System.out.println("狗在吃饭");
}
public String run(){
System.out.println("狗在跑步");
return "跑完了";
}
}
//增强类的后置方法接收目标方法返回的参数
public class AdviceMethod {
public void before(){
System.out.println("主人发出命令");
}
public void after(String arg){
System.out.println("主任给予奖励");
System.out.println(arg);
}
}
配置文件中配置,别的和前面的没区别
<aop:after-returning pointcut-ref="mypointcut" method="after" returning="arg"/>
环绕增强
//定义环绕增强方法,参数为ProceedingJoinPoint,返回值为Object,这是连接点信息,后面会详解
public class AdviceMethod {
public Object around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("环绕增强前");
Object obj=pjp.proceed();
System.out.println("环绕增强后");
return obj;
}
}
<!--配置环绕增强-->
<aop:config proxy-target-class="true">
<aop:aspect ref="advices">
<aop:pointcut expression="execution(* *(..))" id="mypointcut"/>
<aop:around method="around" pointcut-ref="mypointcut"/>
</aop:aspect>
</aop:config>
//我们测试一下
public class TestDemo {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
Dog dog=(Dog)ac.getBean("dog");
dog.eat();
}
}
其他三个配置起来都大同小异,这里就不再一一演示。
增强方法访问连接点信息
AspectJ使用JoinPoint接口表示目标类的连接点对象,如果是环绕增强访问的话,则必须使用ProceedingJoinPoint表示连接点对象,该类是JoinPoint子接口。
JoinPoint接口主要方法
Object [] getArgs():获取连接点方法的参数
Signature getSignatrue():获取连接点的方法签名对象,方法签名由方法名称和形参列表组成。
Object getTarget():获取连接点所在的目标对象
Object getThis():获取代理对象本身
ProceedingJoinPoint主要方法
Object proceed()throws Throwable;通过反射执行目标对象的连接点方法
Object proceed(Object[] args)throws Throwable:通过反射执行目标对象的连接点方法,使用我们提供的参数。
这样,我们就可以在我们的增强方法中使用JoinPoint或者ProceedingJoinPoint参数,来获得连接点方法的一些信息。
AspectJ基于注解配置切面
//先使用注解来实例我们的Bean
@Component("dog")
public class Dog {
public void eat(){
System.out.println("狗在吃饭");
}
public void run(){
System.out.println("狗在跑步");
}
}
//实例增强类
@Component("advices")
//使用Aspect
@Aspect
public class AdviceMethod {
//使用环绕增强,里面参数是切点表达式
@Around("execution(* com.cad.anno.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("环绕增强前");
Object obj=pjp.proceed();
System.out.println("环绕增强后");
return obj;
}
}
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
//使用组件扫描
<context:component-scan base-package="com.cad.anno"></context:component-scan>
//使用aspectj自动代理,自动为匹配@AspectJ切面的Bean创建代理
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
//我们测试一下
public class TestDemo {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
Dog dog=(Dog)ac.getBean("dog");
dog.eat();
}
}
增强类中的方法可以使用不同的增强类型来定义。
- @Before:前置增强
- @AfterReturning:后置增强
- @Around:环绕增强
- @AfterThrowing:抛出异常增强
- @After:Final增强
说点题外话:没想到自己随手整理的笔记,收到了这么多的关注,真的很感谢,能在自己学习的同时给大家提供一点有用的知识。有的错误和不全面的地方也希望大家能指出。自己不是一个很聪明的人,怕自己学习的知识忘得快,便有了记笔记的习惯,前前后后写的也有几十万将近百万的字。每天自己都对着电脑在敲代码,看各种框架的书,也不算太累,就是有的时候会感觉很迷茫,会很担心找不到实习,大三已经要结束了,从未有过考研的念头,对于普通本科学生来说,找工作真的很不容易,只能不断的提高自己,希望大四能找到不错的实习,只是想靠自己努力拿到想要的东西。希望大家都有个好未来,加油。