Java学习笔记-Day65 Spring 框架(三)
一、面向切面编程
1、AOP概念
AOP(Aspect Oriented Programming)面向切面(方面)编程,是一种编程范式,提供从另一个角度来考虑程序结构,从而完善面向对象编程(OOP)。
AOP为开发者提供一种进行横切关注点分离并织入的机制,把横切关注点分离,然后通过某种技术织入到系统中,从而无耦合的完成了功能。
AOP是OOP的补充。面向切面的编程和面向对象并不矛盾,是对面向对象的思维方式的有效补充。面向切面的编程主要将程序中涉及公共问题集中解决,可以解决面向对象和过程化方法中不能很好解决的横切问题,如:事务、安全、日志、异常处理等横切关注。
1.1、方面/切面(Aspect)
切面其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类,之所以能被AOP容器识别成切面,是在全局配置文件中指定的。可以认为是通知、引入和切入点的组合。在Spring中可以使用Schema和@AspectJ方式进行组织实现。
1.2、通知(Advice)
通知是切面的具体实现,在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。
1.3、连接点(Joinpoint)
连接点表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等。
1.4、切入点(Pointcut)
切入点是选择一组相关连接点的模式,可以是认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为“在哪里干的集合“。
1.5、目标对象(Target Object)
目标对象是需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象,由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为“对谁做这件事”。
1.6、代理对象(AOP Proxy Object)
代理对象是指AOP框架使用代理模式创建的对象。代理对象实现在连接点处插入通知(即应用切面),就是通过代理对象来对目标对象应用切面。在Spring中,AOP代理可以用Jdk动态代理(返回接口实例)或CGLib动态代理(返回类实例)实现,而通过拦截器模型应用切面。
1.7、织入(Weaving)
织入是将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器。发生在类装载期,就要求有一个支持这种AOP实现的特殊类装载器。只发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现。
1.8、通知类型
(1)前置通知(Before Advice):在切入点选择的连接点处的方法之前执行的通知,该通知不影响正常程序执行流程(除非该通知抛出异常,该异常将中断当前方法链的执行而返回)。
(2)后置通知(After Advice):在切入点选择的连接点处的方法之后执行的通知,包括如下类型的后置通知:
- 后置返回通知(After returning Advice):在切入点选择的连接点处的方法正常执行完毕时执行的通知,必须是连接点处的方法没抛出任何异常正常返回时才调用后置通知。
- 后置异常通知(After throwing Advice): 在切入点选择的连接点处的方法抛出异常返回时执行的通知,必须是连接点处的方法抛出任何异常返回时才调用异常通知。
- 后置最终通知(After finally Advice): 在切入点选择的连接点处的方法返回时执行的通知,不管抛没抛出异常都执行,类似于Java中的finally块。
(3)环绕通知(Around Advices):环绕着在切入点选择的连接点处的方法所执行的通知,环绕通知可以在方法调用之前和之后自定义任何行为,并且可以决定是否执行连接点处的方法、替换返回值、抛出异常等.
2、代理
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息、过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
代理模式是常用的Java 设计模式,它的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。按照代理类的创建时期,代理类可分为两种,分别是静态代理和动态代理。
二、静态代理和动态代理
1、静态代理
静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的字节码文件就已经存在了。
1.1、实现案例
(1)创建一个Blog实体类
package com.etc.blog.entity;
public class Blog{
private int id;
private String title;
public Blog(int id, String title) {
super();
this.id = id;
this.title = title;
}
public Blog() {
super();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public String toString() {
return "Blog [id=" + id + ", title=" + title + "]";
}
}
(2)创建接口:创建一个BlogDao接口。
package com.etc.blog.dao;
import java.util.List;
import com.etc.blog.entity.Blog;
public interface BlogDao {
public List<Blog> getBlogs();
}
(3)实现接口中的方法:创建BlogDao接口的实现类BlogDaoImpl,并实现BlogDao接口的getBlogs方法 。
package com.etc.blog.dao.impl;
import java.util.List;
import com.etc.blog.dao.BlogDao;
import com.etc.blog.entity.Blog;
public class BlogDaoImpl implements BlogDao{
@Override
public List<Blog> getBlogs() {
System.out.println("BlogDaoImpl getBlogs....");
try {
// 随机暂停几秒
Thread.sleep((long) (Math.random()*10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}
(4)静态代理类的具体实现:创建静态处理类StaticProxy,StaticProxy类实现BlogDao接口和该接口的getBlogs方法
package com.etc.blog.proxy;
import java.util.List;
import com.etc.blog.dao.BlogDao;
import com.etc.blog.entity.Blog;
public class StaticProxy implements BlogDao {
private BlogDao blogdao;
public BlogDao getBlogdao() {
return blogdao;
}
// 全局配置文件中Bean的Setter注入需要使用Setter方法
public void setBlogdao(BlogDao blogdao) {
this.blogdao = blogdao;
}
@Override
public List<Blog> getBlogs() {
//获取当前时间的毫秒值
System.out.println(System.currentTimeMillis());
List<Blog> list = blogdao.getBlogs();
//获取当前时间的毫秒值
System.out.println(System.currentTimeMillis());
return list;
}
}
(5)静态代理在全局配置文件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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="com.etc.blog.dao.impl.BlogDaoImpl" id="blogdao" ></bean>
<bean class="com.etc.blog.service.impl.BlogServiceImpl" autowire="byName"></bean>
<bean class="com.etc.blog.proxy.StaticProxy">
<property name="blogdao" ref="blogdao"></property>
</bean>
</beans>
(6)测试类。
package com.etc.blog.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.etc.blog.proxy.StaticProxy;
@SpringJUnitConfig(locations="classpath:applicationContext.xml")
public class TestStaticProxy {
@Autowired
StaticProxy sp;
@Test
public void test() {
sp.getBlogs();
}
}
1.2、优缺点
优点:业务类只需要关注业务逻辑本身,保证了业务类的重用性。这是代理的共有优点。
缺点: 代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,就要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
2、动态代理
动态代理类:源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。
2.1、实现案例
(1)创建接口:创建一个TargetObjectInterface接口。
package com.etc.blog.entity;
public interface TargetObjectInterface {
public String test(String arg1,String arg2);
}
(2)创建TargetObjectInterface接口的实现类TargetObject ,并实现接口中的test方法。
package com.etc.blog.entity;
public class TargetObject implements TargetObjectInterface{
@Override
public String test(String arg1, String arg2) {
System.out.println("arg1: " + arg1);
System.out.println("arg2: " + arg2);
return arg1 + "," + arg2;
}
}
(3)动态代理类的具体实现:创建DyProxy类,该类实现InvocationHandler接口,并实现接口中invoke方法。
package com.etc.blog.dyproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DyProxy implements InvocationHandler {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(System.currentTimeMillis());
Object result = method.invoke(obj, args);
System.out.println("result="+result);
System.out.println(System.currentTimeMillis());
return result;
}
}
(4)动态代理在全局配置文件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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="com.etc.blog.entity.TargetObject" id="to"></bean>
<bean class="com.etc.blog.dyproxy.DyProxy">
<property name="obj" ref="to"></property>
</bean>
</beans>
(5)测试类(动态代理需要与Proxy类配合使用)。
package com.etc.blog.test;
import java.lang.reflect.Proxy;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.etc.blog.dyproxy.DyProxy;
import com.etc.blog.entity.TargetObjectInterface;
@SpringJUnitConfig(locations = {
"classpath:applicationContext.xml" })
public class TestDyproxy2 {
@Autowired
DyProxy dp;
@Test
public void test() {
// Proxy.newProxyInstance方法
// 第一个参数:被代理对象对应的类加载器
// 第二个参数:被代理的对象
// 第三个参数:实现了InvocationHandler接口的对象,动态代理的具体实现
TargetObjectInterface targetobject = (TargetObjectInterface) Proxy.newProxyInstance(
TargetObjectInterface.class.getClassLoader(), new Class<?>[] {
TargetObjectInterface.class }, dp);
targetobject.test("abc", "xyz");
System.out.println(targetobject);
}
}
2.2、优缺点
优点:动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理 (InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。在本示例中看不出来,因为invoke方法体内嵌入了具体的外围业务(记录任务处理前后时间并计算时间差),实际中可以类似Spring AOP那样配置外围业务。
缺点:Proxy 已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持 interface 代理的束缚,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫 Proxy。Java 的继承机制注定了这些动态代理类们无法实现对 class 的动态代理,原因是多继承在 Java 的本质上就行不通。
三、Spring AOP
1、Spring AOP简介
在Spring中,AOP代理可以用jdk动态代理(返回接口实例)或CGLib动态代理(返回类实例)实现,默认使用是jdk的动态代理。
2、基于XML的Spring AOP配置
xml方式是以后使用的比较多的,因为当切面类没有源代码时,使用第三方的切面类就不能使用annotation的方式,而且如果使用annotation方式,一但程序编译后就不可以修改了,如果使用xml方式就不一样了,只需要修改xml文件就可以了。
在Spring配置文件中,所有AOP相关定义必须放在<aop:config>
标签下,<aop:config>
为AOP的根标签,该标签下可以有<aop:pointcut>
、<aop:advisor>
、<aop:aspect>
标签,配置顺序不可变。
(1)<aop:advisor>
:用来定义只有一个通知和一个切入点的切面。
(2)<aop:pointcut>
:用来定义切入点,该切入点可以重用。切入点 expression属性用于定义切入点模式,默认是AspectJ语法 execution(* com.etc.service.*.*(..))
表示匹配 com.etc.service 包及子包下的任何方法执行,第一个*
代表所有的返回值类型,第二个*
代表所有的类,第三个*
代表类所有方法,最后一个..
代表所有的参数(有参数与无参数)。
<aop:pointcut expression="execution(* com.etc.spring.aop.impl.*.*(..))" id="mypointcut"/>
(3)<aop:aspect>
:用来定义切面,该切面可以包含多个切入点和通知,而且标签内部的通知和切入点定义是无序的。和advisor的区别就在此,advisor只包含一个通知和一个切入点。切面使用<aop:config>
标签下的<aop:aspect>
标签配置,其中ref属性用来引用切面支持类的方法
(4)<aop:before>
:前置通知使用<aop:aspect>
标签下的<aop:before>
标签来定义,pointcut-ref属性用于引用切入点Bean,而method用来引用切面通知实现类中的方法,该方法就是通知实现,即在目标类方法执行之前调用的方法。
(5)<aop:after>
:后置通知使用<aop:aspect>
标签下的<aop:after>
标签来定义,切入点除了使用pointcut-ref属性来引用已经存在的切入点,也可以使用pointcut属性来定义。
(6)<aop:around>
:环绕通知使用<aop:aspect>
标签下的<aop:around>
标签来定义,切入点除了使用pointcut-ref属性来引用已经存在的切入点,也可以使用pointcut属性来定义。
<aop:aspect ref="myaop">
<aop:before method="before" pointcut-ref="mypointcut"/>
<aop:after method="after" pointcut-ref="mypointcut"/>
<aop:around method="around" pointcut-ref="mypointcut"/>
</aop:aspect>
//前置通知的方法
public void before() {
System.out.println(System.currentTimeMillis());
}
//后置通知的方法
public void after() {
System.out.println(System.currentTimeMillis());
}
//环绕通知的方法
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around方法前。。。");
Object result = pjp.proceed();
System.out.println("around方法后。。。");
return result;
}
2.1、jdk动态代理
操作步骤:
(1)导入外部jar包:aopalliance-1.0.jar 和 aspectjweaver-1.9.2.jar。
(2)创建目标接口。
package com.etc.spring.aop;
public interface IMessageService {
public void sendMessage();
}
(3)创建目标接口的实现类。
package com.etc.spring.aop.impl;
import com.etc.spring.aop.IMessageService;
public class MessageServiceImpl implements IMessageService {
@Override
public void sendMessage() {
System.out.println("send message.....");
}
}
(4)创建切面的支持类。
package com.etc.spring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAop {
//前置通知 sendMessage之前
public void before() {
System.out.println(System.currentTimeMillis());
}
//后置通知 sendMessage之后
public void after() {
System.out.println(System.currentTimeMillis());
}
//环绕通知
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around方法前。。。");
Object result = pjp.proceed();
System.out.println("around方法后。。。");
return result;
}
}
(5)在aop.xml全局配置文件中配置Aop。
<?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: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/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<bean class="com.etc.spring.aop.MyAop" id="myaop"></bean>
<bean class="com.etc.spring.aop.impl.MessageServiceImpl" id="msi"></bean>
<!-- 使用jdk动态代理(返回接口实例) -->
<aop:config>
<!-- 切入点,在目标对象的位置织入通知-->
<aop:pointcut expression="execution(* com.etc.spring.aop.impl.*.*(..))" id="mypointcut"/>
<aop:aspect ref="myaop">
<aop:before method="before" pointcut-ref="mypointcut"/>
<aop:after method="after" pointcut-ref="mypointcut"/>
<aop:around method="around" pointcut-ref="mypointcut"/>
</aop:aspect>
</aop:config>
</beans>
(6)测试类
package com.etc.blog.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.etc.spring.aop.IMessageService;
@SpringJUnitConfig(locations = {
"classpath:aop.xml" })
public class TestMessage {
@Autowired
IMessageService ims;
@Test
public void test() {
ims.sendMessage();
}
}
2.2、CGLib动态代理
CGLib动态代理的步骤与jdk动态代理的步骤基本相同,只有全局配置文件和测试类不同。
操作步骤:
(1)导入外部jar包:aopalliance-1.0.jar 和 aspectjweaver-1.9.2.jar。
(2)创建目标接口。
package com.etc.spring.aop;
public interface IMessageService {
public void sendMessage();
}
(3)创建目标接口的实现类。
package com.etc.spring.aop.impl;
import com.etc.spring.aop.IMessageService;
public class MessageServiceImpl implements IMessageService {
@Override
public void sendMessage() {
System.out.println("send message.....");
}
}
(4)创建切面的支持类。
package com.etc.spring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAop {
//前置通知 sendMessage之前
public void before() {
System.out.println(System.currentTimeMillis());
}
//后置通知 sendMessage之后
public void after() {
System.out.println(System.currentTimeMillis());
}
//环绕通知
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around方法前。。。");
Object result = pjp.proceed();
System.out.println("around方法后。。。");
return result;
}
}
(5)在aop.xml全局配置文件中配置Aop。
<?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: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/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 使用cglib代理类(返回类实例) -->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
<bean class="com.etc.spring.aop.impl.MessageServiceImpl" id="msi"></bean>
<bean class="com.etc.spring.aop.MyAop" id="myaop"></bean>
<aop:config>
<!-- 切入点,在目标对象的位置织入通知-->
<aop:pointcut expression="execution(* com.etc.spring.aop.impl.*.*(..))" id="mypointcut"/>
<aop:aspect ref="myaop">
<aop:before method="before" pointcut-ref="mypointcut"/>
<aop:after method="after" pointcut-ref="mypointcut"/>
<aop:around method="around" pointcut-ref="mypointcut"/>
</aop:aspect>
</aop:config>
</beans>
(6)测试类。
package com.etc.blog.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.etc.spring.aop.impl.MessageServiceImpl;
@SpringJUnitConfig(locations = {
"classpath:aop2.xml" })
public class TestMessage2 {
@Autowired
MessageServiceImpl msi;
@Test
public void test() {
msi.sendMessage();
}
}