一、AOP概述
1.AOP
全称是Aspect Oriented Programming即:面向切面编程。
简单的说它就是把程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。
2.作用
在程序运行期间,不修改源码对已有方法进行增强。
①减少重复代码
②提高开发效率
③维护方便
AOP的实现方式
使用动态代理技术
二、动态代理
1.动态代理的特点
字节码随用随创建,随用随加载。
它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。
装饰者模式就是静态代理的一种体现。
2.动态代理常用的两种方式
- 基于接口的动态代理
提供者:JDK官方的Proxy类。
要求:被代理类最少实现一个接口。
创建代理对象的方法:newProxyInstance(ClassLoader,Class[],InvocationHandler)
参数的含义:
ClassLoader
:类加载器。和被代理对象使用相同的类加载器。一般都是固定写法
Class[]
:字节码数组。被代理类实现的接口。要求代理对象和被代理对象有相同的行为。一般都是固定写法。
InvocationHandler
:它是一个接口,就是用于提供增强代码的。一般都是写一个该接口的实现类。实现类可以是匿名内部类。
含义:如何代理。这个参数的代码只能是谁用谁提供。
策略模式:数据有,目标明确,达成目标的过程就是策略。
举一个演员和经纪公司的例子
定义一个艺人标准的接口IActor.java
package com.company.proxy;
/**
* 艺人标准
*/
public interface IActor {
public void basicAct(float money);
public void dangerAct(float money);
}
再实现一个演员类Actor.java
package com.company.proxy;
/**
* 一个演员
*/
public class Actor implements IActor{
public void basicAct(float money){
System.out.println("拿到钱,开始基本表演"+money);
}
public void dangerAct(float money){
System.out.println("拿到钱,开始危险表演"+money);
}
}
最后再来个经纪公司管理演员的出场费用,并使用动态代理
Client.java
package com.company.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 模拟一个经纪公司
*/
public class Client {
public static void main(String[] args) {
Actor actor = new Actor();
//没有经纪公司的时候,直接找演员。
// actor.basicAct(100f);
// actor.dangerAct(500f);
//动态代理
IActor proxyActor = (IActor) Proxy.newProxyInstance(actor.getClass().getClassLoader(),
actor.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 执行被代理对象的任何方法都会经过该方法,该方法有拦截的功能
* 方法的参数
* Object proxy:代理对象的引用。不一定每次都会有
* Method method:当前执行的方法
* Object[] args:当前执行方法所需的参数
* 返回值:
* 当前执行方式的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
//1.取出执行方法中的参数
Float money = (Float) args[0];
//2.判断当前执行的是什么方法
//每个经纪公司对不同演出收费不一样,此处开始判断
if ("basicAct".equals(method.getName())){
//基本演出
if (money > 10000){
//执行方法
//看上去剧组是给了8000,实际到演员手里只有4000
//这就是我们没有修改原来basicAct方法源码,对方法进行了增强
rtValue = method.invoke(actor,money/2);
}
}
if ("dangerAct".equals(method.getName())){
//危险演出
if (money > 50000){
//执行方法
rtValue = method.invoke(actor,money);
}
}
return rtValue;
}
});
//剧组无法直接联系演员,而是由经纪公司找的演员
proxyActor.basicAct(80000f);
proxyActor.dangerAct(60000f);
}
}
- 基于子类的动态代理
提供者:第三方的CGLib,如果报asmxxxx异常,需要导入asm.jar。
要求:被代理类不能用final修饰的类(最终类)。
涉及的类:Enhancer
创建代理对象的方法:create(Class,Callback);
参数的含义:
Class
:被代理对象的字节码
Callback
:如何代理。它和InvocationHandler的作用一样。
它也是一个接口,一般使用该接口的子接口MethodInterceptor,在使用是也是创建该接口的匿名内部类。
还是上述例子
不需要IActor.java的接口
更改Client.java
package com.company.proxy;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 模拟一个经纪公司
*/
public class Client {
public static void main(String[] args) {
Actor actor = new Actor();
//动态代理
Actor cglibActor = (Actor) Enhancer.create(actor.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
/**
* 执行被代理对象的任何方法都会经过该方法。它和基于接口动态代理的invoke方法的作用一样
* 方法的参数:
* 前面三个和invoke方法的参数含义和作用一样。
* MethodProxy methodProxy:当前执行方法的代理对象,一般不用
*/
Object rtValue = null;
//1.取出执行方法中的参数
Float money = (Float) objects[0];
//2.判断当前执行的是什么方法
//每个经纪公司对不同演出收费不一样,此处开始判断
if ("basicAct".equals(method.getName())){
//基本演出
if (money > 10000){
//执行方法
//看上去剧组是给了8000,实际到演员手里只有4000
//这就是我们没有修改原来basicAct方法源码,对方法进行了增强
rtValue = method.invoke(actor,money/2);
}
}
if ("dangerAct".equals(method.getName())){
//危险演出
if (money > 50000){
//执行方法
rtValue = method.invoke(actor,money);
}
}
return rtValue;
}
});
cglibActor.basicAct(50000);
cglibActor.dangerAct(100000);
}
}
3.动态代理的应用——连接池原理(c3p0)
导入的jar包要mysql-connector-java-5.0.8-bin.jar
因为这个包里的Connection是个类,而5.1.7的版本里Connection是个接口,无法强转。
关闭一个连接就少一个,启用动态代理,让用完的连接还回池中
项目目录结构
dbconfig.properties
#数据库连接的配置
#数据库驱动
DRIVER=com.mysql.jdbc.Driver
#连接字符串
URL=jdbc:mysql://localhost:3306/goods
#数据库用户名
USER=root
#数据库密码
PASSWORD=root
JDBCUtil.java
package com.edu.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ResourceBundle;
/**
* 数据库操作相关的工具类
* @author zhy
*
*/
public class JDBCUtil {
//使用ResourceBundle读取资源文件
private static ResourceBundle bundle = ResourceBundle.getBundle("dbconfig");
private static String driver;
private static String url;
private static String user;
private static String password;
//使用静态代码块进行赋值
static{
driver = bundle.getString("DRIVER");
url = bundle.getString("URL");
user = bundle.getString("USER");
password = bundle.getString("PASSWORD");
}
/**
* 获取连接
* @return
*/
public static Connection getConnection(){
Connection conn = null;
try {
Class.forName(driver);
conn = DriverManager.getConnection(url, user, password);
return conn;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 释放资源
* @param rs
* @param st
* @param conn
*/
public static void release(ResultSet rs,Statement st,Connection conn){
if(rs != null){
try{
rs.close();
}catch(Exception e){
e.printStackTrace();
}
}
if(st != null){
try{
st.close();
}catch(Exception e){
e.printStackTrace();
}
}
if(conn != null){
try{
conn.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
自定义连接池MyDataSource.java
package com.edu.dataSource;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.edu.util.JDBCUtil;
/**
* 自定义连接池
* @author zhy
*
*/
public class MyDataSource {
//定义一个池,用于存放连接
private static List<Connection> pool = Collections.synchronizedList(new ArrayList<Connection>());//把ArrayList转成线程安全的
//使用静态代码块给池中加入连接
static{
for(int i=0;i<10;i++){
Connection conn = JDBCUtil.getConnection();
pool.add(conn);
}
}
/**
* 获取一个连接
* @return
*/
public static Connection getConnection(){
final Connection conn = pool.remove(0);
//创建代理对象
Connection proxyConn = (Connection) Proxy.newProxyInstance(conn.getClass().getClassLoader(),
conn.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
//1.判断当前方法是不是close方法
if("close".equals(method.getName())){
//不能直接关闭
pool.add(conn);//还回池中
}else{
rtValue = method.invoke(conn, args);
}
return rtValue;
}
});
return proxyConn;
}
/**
* 获取池中的连接数
* @return
*/
public static int getPoolSize(){
return pool.size();
}
}
测试类
package com.edu.test;
import com.edu.dataSource.MyDataSource;
import java.sql.Connection;
public class Main {
public static void main(String[] args) throws Exception {
int size = MyDataSource.getPoolSize();
System.out.println("使用连接之前"+size);
for (int i = 0;i < 8 ;i++) {
Connection conn = MyDataSource.getConnection();
System.out.println(conn);
conn.close();
}
int size1 = MyDataSource.getPoolSize();
System.out.println("使用连接之后"+size1);
for (int i = 0;i < 8 ;i++) {
Connection conn = MyDataSource.getConnection();
System.out.println(conn);
conn.close();
}
}
}
三、Spring中的AOP
1.AOP相关术语
- Joinpoint(连接点)
业务层的接口方法,一端:公共代码(增强的代码);另一端就是业务,究竟要干什么事。这个连接点就是将业务和增强的代码结合起来。 - Pointcut(切入点):
被增强的就是切入点,没被增强的就是连接点。
连接点不一定是切入点;切入点一定是连接点。 - Advice(通知/增强):
增强的代码在哪个类,哪个类就是通知。
通知类型:前置通知,后置通知,异常通知,最终通知,环绕通知。 - Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field。 - Target(目标对象):
代理的目标对象。 - Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程。
spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。 - Proxy(代理):
一个类被AOP织入增强后,就产生一个结果代理类。 - Aspect(切面):
是切入点和通知(引介)的结合。
2.基于XML的AOP环境搭建
所需jar包
目录结构
需要加强的类:
package com.edu.utils;
public class Logger {
/**
* 记录日志的操作
* 计划让其在业务核心方法(切入点方法)执行之前执行
*/
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("Logger中的beforePrintLog方法开始记录日志了。。。。");
}
/**
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("Logger中的afterReturningPrintLog方法开始记录日志了。。。。");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("Logger中的afterThrowingPrintLog方法开始记录日志了。。。。");
}
/**
* 最终通知
*/
public void afterPrintLog(){
System.out.println("Logger中的afterPrintLog方法开始记录日志了。。。。");
}
}
配置bean.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"
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.xsd">
<!-- 配置Service -->
<bean id="customerService" class="com.edu.service.impl.CustomerImpl"></bean>
<!-- 基于XML的aop配置 -->
<!-- 1.把通知类交给spring管理 -->
<bean id="Logger" class="com.edu.utils.Logger"></bean>
<!-- 2.导入aop名称空间,并且使用aop:config开始aop的配置 -->
<aop:config>
<!-- 定义通用的切入点表达式,如果写在aop:aspct标签外部,则表示所有切面可用 -->
<aop:pointcut expression="execution(* com.edu.service.impl.*.*(..))" id="pt1"/>
<!-- 3.使用aop:aspect配置切面,id属性用于给切面提供一个唯一标识。ref属性:用于通知bean的id -->
<aop:aspect id="logAdvice" ref="Logger">
<!-- 4.配置通知的类型,指定增强的方法什么时候执行。
method属性:用于指定的方法名称,
pointcut:用于指定切入点表达式
-->
<!-- 切入点表达式:
关键字:execution(表达式)
表达式写法:
访问修饰符 返回值 报名.报名...类名.方法名(参数列表)
全匹配方法:
public void com.edu.service.impl.CustomerImpl.saveCustomer()
访问修饰符可以省略
void com.edu.service.impl.CustomerServiceImpl.saveCustomer()
返回值可以使用*号,表示任意返回值
* com.edu.service.impl.CustomerServiceImpl.saveCustomer()
包名可以使用*号,表示任意包,但是有几级包,需要写几个*
* *.*.*.*.CustomerServiceImpl.saveCustomer()
使用..来表示当前包,及其子包
* com..CustomerServiceImpl.saveCustomer()
类名可以使用*号,表示任意类
* com..*.saveCustomer()
方法名可以使用*号,表示任意方法
* com..*.*()
参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
* com..*.*(*)
参数列表可以使用..表示有无参数均可,有参数可以是任意类型
* com..*.*(..)
全通配方式:
* *..*.*(..)
-->
<!-- 配置前置通知: 永远在切入点方法执行之前执行 -->
<aop:before method="beforePrintLog" pointcut-ref="pt1"/>
<!-- 配置后置通知: 切入点方法正常执行之后执行 -->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"/>
<!-- 配置异常通知: 切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个 -->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"/>
<!-- 配置最终通知:无论切入点方法是否正常执行,它都会在其后面执行 -->
<aop:after method="afterPrintLog" pointcut-ref="pt1"/>
<!-- 定义通用的切入点表达式:如果只写在了aop:aspect标签内部,则表达式只有当前切面可用-->
<!--<aop:pointcut id="pt1" expression="execution(* com.edu.service.impl.*.*(..))"/>-->
</aop:aspect>
</aop:config>
</beans>
ui界面:
package com.edu.ui;
import com.edu.service.ICustomerService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Client {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
ICustomerService cs = (ICustomerService) applicationContext.getBean("customerService");
cs.saveCustomer();
}
}
环绕通知
/**
* 环绕通知
* 它是spring框架为我们提供的一种可以在代码中手动控制增强部分什么时候执行的方式。
* 问题:
* 当我们配置了环绕通知之后,增强的代码执行了,业务核心方法没有执行。
* 分析:
* 通过动态代理我们知道在invoke方法中,有明确调用业务核心方法:method.invoke()。
* 我们配置的环绕通知中,没有明确调用业务核心方法。
* 解决:
* spring框架为我们提供了一个接口:ProceedingJoinPoint,它可以作为环绕通知的方法参数
* 在环绕通知执行时,spring框架会为我们提供该接口的实现类对象,我们直接使用就行。
* 该接口中有一个方法proceed(),此方法就相当于method.invoke()
*/
public void aroundPringLog(ProceedingJoinPoint pjp){
try {
System.out.println("前置通知:Logger类的aroundPringLog方法记录日志");
pjp.proceed();
System.out.println("后置通知:Logger类的aroundPringLog方法记录日志");
} catch (Throwable e) {
System.out.println("异常通知:Logger类的aroundPringLog方法记录日志");
e.printStackTrace();
}finally{
System.out.println("最终通知:Logger类的aroundPringLog方法记录日志");
}
}
bean.xml
<aop:around method="aroundPrintLog" pointcut-ref="pt1"/>
3.基于注解的AOP环境搭建
上述案例
加强的类:
package com.edu.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 一个用于记录日志的类
*
*/
@Component("logger")
@Aspect//配置了切面
public class Logger {
@Pointcut("execution(* com.edu.service.impl.*.*(..))")
private void pt1(){}
/**
* 前置通知
*/
//@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置:Logger中的beforePrintLog方法开始记录日志了。。。。");
}
/**
* 后置通知
*/
//@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置:Logger中的afterReturningPrintLog方法开始记录日志了。。。。");
}
/**
* 异常通知
*/
//@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常:Logger中的afterThrowingPrintLog方法开始记录日志了。。。。");
}
/**
* 最终通知
*/
//@After("pt1()")
public void afterPrintLog(){
System.out.println("最终:Logger中的afterPrintLog方法开始记录日志了。。。。");
}
/**
* 环绕通知
*/
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。前置");
rtValue = pjp.proceed();
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。后置");
} catch (Throwable e) {
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。异常");
e.printStackTrace();
}finally{
System.out.println("Logger中的aroundPrintLog方法开始记录日志了。。。。最终");
}
return rtValue;
}
}
bean.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"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring创建容器时要扫描的包 -->
<context:component-scan base-package="com.edu"></context:component-scan>
<!-- 开启spring对注解AOP的支持-->
<aop:aspectj-autoproxy />
</beans>
纯注解
package config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
Client.java
package com.itheima.ui;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.itheima.service.ICustomerService;
import config.SpringConfiguration;
public class Client {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
ICustomerService cs = (ICustomerService) ac.getBean("customerService");
cs.saveCustomer();
}
}