软件工程上要求:关注点分离,也即不同的问题交给不同的部分去解决。
与OOP对比,面向切面,传统的OOP开发中的代码逻辑是自上而下的,而这些过程会产生一些横切性问题,这些横切性的问题和我们的主业务逻辑关系不大,这些横切性问题不会影响到主逻辑实现的,但是会散落到代码的各个部分,难以维护。AOP是处理一些横切性问题,AOP的编程思想就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。使代码的重用性和开发效率更高。
直白的来说:
①面向切面编程编程AOP正实此技术的实现
②通用功能的代码实现,对应的就是技术的实现
③Aop技术使业务功能代码与通用代码分开,让项目变得高内聚低耦合
① 日志记录
② 权限验证
③ 效率检查
④ 事务管理
⑤ exception
在Spring官方开发手册上,对概念的解释也十分"性感",Spring认为单从概念来说相对模糊,要从应用入手,那么下面本文将会从应用逐渐揭开Aop的神秘面纱。
Aop常见的切面编程,最常见的引用Aop技术提供的Annotation标签进行注解,那么我们如果想要看懂源码,就需要了解一下Annotation的原理,我们下面自己写一个Annotation。
自写MyAnnotation.class
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//指定当前Annotation的标记范围(类,属性,方法等)
@Target(ElementType.TYPE)
//保留政策,如果不设置的话默认是Source,在编译字节码阶段,会失去Annotation,反射机制无法知道
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
//注解中所写的方法相当于需要在Annotation给定的值,例如:@MyAnnotation(value="city")
//Default表示,如果不给定值,默认的是多少
public String value() default "";
public String name() default "";
}
在自己写Annotation,重点在于三点
① @Target,这是Java API提供的,这个标签的作用是限定此Annotation目标有效于类,属性抑或是方法等。
② @Retention主要是保留政策,如果默认的话,那么在字节码编译中会丢失,导致反射机制认为此类没有Annotation。建议设置为RetentionPolicy.RUNTIME
③ 类中的方法,在别的类引用此Annotation时,可以进行赋值,如果不赋值,会使用默认值
引用类
//给予Annotation标签类方法的值
@MyAnnotation(value = "city",name ="haha")
public class Sql {
//需要查询人的名字
private String name;
//需要查询人的年龄
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
反射调用Annotation
public class SqlUtil {
public static String getSql(Object object) throws Exception{
//如果有Annotation
if (object.getClass().isAnnotationPresent(MyAnnotation.class)){
//利用反射机制获取类的Annotation的类
MyAnnotation myAnnotation=
object.getClass().getAnnotation(MyAnnotation.class);
String AnnotationDate=myAnnotation.value();
//返回组合成的Sql语句
return "select * from" +AnnotationDate;
}else {
return "false";
}
}
}
Spring开发手册对Aop得详细解析
我们来重点分析下,Aop常用的概念
①Aspect(切面):通用代码的实现,可以理解为一系列切点,和一系列织入增强的模块。
②Target(目标对象):被织入Aspect的对象。
③Join Point(连接点): 可以作为切入点的机会。程序执行过程中的一个点,例如方法执行或异常处理。在Spring AOP中,连接点始终代表方法的执行。
④Point Cut(切点):Aspect实际作用的地点,支持正则表达式定义切入点。
⑤Weaving(织入):把代理逻辑加入到目标对象上的过程叫做织入。
⑥Advice(通知):类的方法以及此方法如何切入切点的定义。例如:“around”,“brfore”,"after"等。
通知类型 | 解释 |
---|---|
Before | 连接点执行之前,但是无法阻止连接点的正常执行,除非该段执行抛出异常 |
After | 连接点正常执行之后,执行过程中正常执行返回退出,非异常退出 |
After throwing | 执行抛出异常的时候 |
After (finally) | 无论连接点是正常退出还是异常退出,都会执行 |
Around advice | 围绕连接点执行,例如方法调用。这是最有用的切面方式。 |
around | 通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续加入点还是通过返回自己的返回值或抛出异常来快速建议的方法执行。 |
上述概念听后,对我们理解只是起到一定的作用,想要完全理解还需要实际去使用。下面我写一下Aop使用的例子。
Aop,Spring Aop,AspectJ三者的关系是什么 ?
在我们打开Spring Aop的文档第一页便进行了解释。
Aop是一种面向切面编程的思想,而Spring Aop和AspectJ是这种思想的实现,Spring Aop在早期版本中,是没有参照AspectJ的语法,实际开发中,反响不好。所以在后期参照了AspectJ的语法,但是底层实现还是Spring的技术。
//在JavaConfigure上进行配置
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
申明一个@Aspect注释类,并且定义成一个bean交给Spring管理。
@Component
@Aspect
public class UserAspect {
}
切入点表达式由@Pointcut注释表示。切入点声明由两部分组成:一个签名包含名称和任何参数,以及一个切入点表达式,该表达式确定我们对哪个方法执行感兴趣。
/**
* 申明Aspect,并且交给spring容器管理
*/
@Component
@Aspect
public class UserAspect {
/**
* 申明切入点,匹配UserDao所有方法调用
* execution匹配方法执行连接点
* within:将匹配限制为特定类型中的连接点
* args:参数
* target:目标对象
* this:代理对象
*/
@Pointcut("execution(* com.yao.dao.UserDao.*(..))")
public void pintCut(){
System.out.println("point cut");
}
各种连接点joinPoint的意义:
1.execution
//问号表示当前项可以有,可以没有
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
名称 | 意义 |
---|---|
modifiers-pattern | 方法的可见性,如public,protected |
ret-type-pattern | 方法的返回值类型,如int,void等 |
declaring-type-pattern | 方法所在类的全路径名,如com.spring.Aspect |
name-pattern | 方法名类型,如buisinessService() |
param-pattern | 方法的参数类型,如java.lang.String |
throws-pattern | 方法抛出的异常类型,如java.lang.Exception |
spring开发文档提供了可供参考的例子
@Pointcut("execution(* com.chenss.dao.*.*(..))")//匹配com.chenss.dao包下的任意接口和类的任意方法
@Pointcut("execution(public * com.chenss.dao.*.*(..))")//匹配com.chenss.dao包下的任意接口和类的public方法
@Pointcut("execution(public * com.chenss.dao.*.*())")//匹配com.chenss.dao包下的任意接口和类的public 无方法参数的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String, ..))")//匹配com.chenss.dao包下的任意接口和类的第一个参数为String类型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")//匹配com.chenss.dao包下的任意接口和类的只有一个参数,且参数为String类型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")//匹配com.chenss.dao包下的任意接口和类的只有一个参数,且参数为String类型的方法
@Pointcut("execution(public * *(..))")//匹配任意的public方法
@Pointcut("execution(* te*(..))")//匹配任意的以te开头的方法
@Pointcut("execution(* com.chenss.dao.IndexDao.*(..))")//匹配com.chenss.dao.IndexDao接口中任意的方法
@Pointcut("execution(* com.chenss.dao..*.*(..))")//匹配com.chenss.dao包及其子包中任意的方法
2.within
表达式的最小粒度为类
// within与execution相比,粒度更大,仅能实现到包和接口、类级别。而execution可以精确到方法的返回值,参数个数、修饰符、参数类型等
@Pointcut("within(com.chenss.dao.*)")//匹配com.chenss.dao包中的任意方法
@Pointcut("within(com.chenss.dao..*)")//匹配com.chenss.dao包及其子包中的任意方法
execution和within的区别是什么?
within只可以匹配到类,而execution可以匹配到类的方法以及方法的类型(public,private…)以及参数类型等。execution的粒度相对于within更小。
3.args
args表达式的作用是匹配指定参数类型和指定参数数量的方法,与包名和类名无关。args同execution不同的地方在于:args匹配的是运行时传递给方法的参数类型
/**
* execution(* *(java.io.Serializable))匹配的是方法在声明时指定的方法参数类型。
*/
@Pointcut("args(java.io.Serializable)")//匹配运行时传递的参数类型为指定类型的、且参数个数和顺序匹配
@Pointcut("@args(com.chenss.anno.Chenss)")//接受一个参数,并且传递的参数的运行时类型具有@Classified
4.this
this与JDK instanceof作用相似,判断对象是否是this指向类的实例,JDK代理时,指向接口和代理类proxycglib代理时 指向接口和子类(不使用proxy)。具体是因为SpringAOP底层JDK代理和Cglib的不同,详情参考下方章节。
@this(com.sheng.service)
5.target
目标对象,也就是被代理的对象。指向被代理的对象,与this不同点在于不受底层实现的影响。
/**
* 此处需要注意的是,如果配置设置proxyTargetClass=false,或默认为false,则是用JDK代理,否则使用的是CGLIB代理
* JDK代理的实现方式是基于接口实现,代理类继承Proxy,实现接口。
* 而CGLIB继承被代理的类来实现。
* 所以使用target会保证目标不变,关联对象不会受到这个设置的影响。
* 但是使用this对象时,会根据该选项的设置,判断是否能找到对象。
*/
@Pointcut("target(com.chenss.dao.IndexDaoImpl)")//目标对象,也就是被代理的对象。限制目标对象为com.chenss.dao.IndexDaoImpl类
@Pointcut("this(com.chenss.dao.IndexDaoImpl)")//当前对象,也就是代理对象,代理对象时通过代理目标对象的方式获取新的对象,与原值并非一个
@Pointcut("@target(com.chenss.anno.Chenss)")//具有@Chenss的目标对象中的任意方法
@Pointcut("@within(com.chenss.anno.Chenss)")//等同于@targ
advice通知与pointcut切入点表达式相关联,并在切入点匹配的方法执行@Before之前、@After之后或前后运行。
/**
* 申明Aspect,并且交给spring容器管理
*/
@Component
@Aspect
public class UserAspect {
/**
* 申明切入点,匹配UserDao所有方法调用
* execution匹配方法执行连接点
* within:将匹配限制为特定类型中的连接点
* args:参数
* target:目标对象
* this:代理对象
*/
@Pointcut("execution(* com.yao.dao.UserDao.*(..))")
public void pintCut(){
System.out.println("point cut");
}
/**
* 申明before通知,在pintCut切入点前执行
* 通知与切入点表达式相关联,
* 并在切入点匹配的方法执行之前、之后或前后运行。
* 切入点表达式可以是对指定切入点的简单引用,也可以是在适当位置声明的切入点表达式。
*/
@Before("com.yao.aop.UserAspect.pintCut()")
public void beforeAdvice(){
System.out.println("before");
}
}
通知类型 | 解释 |
---|---|
Before | 连接点执行之前,但是无法阻止连接点的正常执行,除非该段执行抛出异常 |
After | 连接点正常执行之后,执行过程中正常执行返回退出,非异常退出 |
After throwing | 执行抛出异常的时候 |
After (finally) | 无论连接点是正常退出还是异常退出,都会执行 |
Around advice | 围绕连接点执行,例如方法调用。这是最有用的切面方式。around通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续加入点还是通过返回自己的返回值或抛出异常来快速建议的方法执行。 |
Proceedingjoinpoint 和JoinPoint的区别:
Proceedingjoinpoint 继承了JoinPoint,proceed()这个是aop代理链执行的方法。并扩充实现了proceed()方法,用于继续执行连接点。JoinPoint仅能获取相关参数,无法执行连接点。
JoinPoint的方法
1.java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
2.Signature getSignature() :获取连接点的方法签名对象;
3.java.lang.Object getTarget() :获取连接点所在的目标对象;
4.java.lang.Object getThis() :获取代理对象本身;
proceed()有重载,有个带参数的方法,可以修改目标方法的的参数 是环绕通知连接点的核心参数类,可以让环绕通知定义在何时执行Aop目标被代理对象的方法。
Proceedingjoinpoint支持在环绕通知(Around Advice)使用。
//增强通知主要应用于对参数的处理
@Around("PointCut()")
public void myAround(ProceedingJoinPoint psj){
System.out.println("方法执行前要干的事情");
try {
//接收代理对象的参数
Object[] objects= psj.getArgs();
//对参数进行处理
for (int i=0;i<objects.length;i++){
objects[i]+="world";
}
//执行处理后的参数
psj.proceed(objects);
}catch (Throwable e){
e.printStackTrace();
}
System.out.println("方法执行后要干的事情");
}
继承代理
继承目标对象,在原有功能基础上进行代理拓展
//继承目标对象
public class ProxyObject extends TargetObject {
public void query(){
//拓展代码
System.out.println("此处是拓展代码");
//目标对象原功能代码
System.out.println("目标对象原功能代码");
}
聚合代理
代理对象要与目标对象实现同一接口,并包含目标对象的实例对象。
//实现同一接口
public class ProxyObject implements TargetInterface {
//构造方法
public ProxyObject(TargetObject targetObject){
this.targetObject=targetObject;
}
//目标对象
public TargetObject targetObject;
public void query(){
//拓展代码
System.out.println("此处是拓展代码");
//目标对象原功能代码
targetObject.query();
}
目标对象与代理对象
根据开闭原则以及单一原则,功能建议拓展而不是修改,所以功能拓展一般采用代理对象模式。目标对象和代理对象都是相对的,会根据情况进行变化。
动态代理的思想:
根据代码的规则以及反射机制,动态的组合参数名称,方法名称以及类信息等。利用IO流输这些字符流,利用JDK编译器进行编译,最后通过类加载器加载为对象。即完成动态的代理流程。
参考代码:
public class ProxyUtil {
public static Object newInstance(Object target){
//利用反射机制获得各种所需的参数,方法,接口等等名称组成一段完整有效代码的String
Object proxy=null;
Class targetInf = target.getClass().getInterfaces()[0];
Method methods[] =targetInf.getDeclaredMethods();
String line="\n";
String tab ="\t";
String infName = targetInf.getSimpleName();
String content ="";
String packageContent = "package com.google;"+line;
String importContent = "import "+targetInf.getName()+";"+line;
String clazzFirstLineContent = "public class $Proxy implements "+infName+"{"+line;
String filedContent =tab+"private "+infName+" target;"+line;
String constructorContent =tab+"public $Proxy ("+infName+" target){" +line
+tab+tab+"this.target =target;"
+line+tab+"}"+line;
String methodContent = "";
for (Method method : methods) {
String returnTypeName = method.getReturnType().getSimpleName();
String methodName =method.getName();
// Sting.class String.class
Class args[] = method.getParameterTypes();
String argsContent = "";
String paramsContent="";
int flag =0;
for (Class arg : args) {
String temp = arg.getSimpleName();
//String
//String p0,Sting p1,
argsContent+=temp+" p"+flag+",";
paramsContent+="p"+flag+",";
flag++;
}
if (argsContent.length()>0){
argsContent=argsContent.substring(0,argsContent.lastIndexOf(",")-1);
paramsContent=paramsContent.substring(0,paramsContent.lastIndexOf(",")-1);
}
methodContent+=tab+"public "+returnTypeName+" "+methodName+"("+argsContent+") {"+line
+tab+tab+"System.out.println(\"log\");"+line
+tab+tab+"target."+methodName+"("+paramsContent+");"+line
+tab+"}"+line;
}
content=packageContent+importContent+clazzFirstLineContent+filedContent+constructorContent+methodContent+"}";
File file =new File("d:\\com\\google\\$Proxy.java");
try {
//组合好的String写入文件
if (!file.exists()) {
file.createNewFile();
}
FileWriter fw = new FileWriter(file);
fw.write(content);
fw.flush();
fw.close();
//JDK第三方编译器,对.java文件进行编译
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
Iterable units = fileMgr.getJavaFileObjects(file);
JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
t.call();
fileMgr.close();
//ClassLoder类加载器,对编译好的.class 进行加载
URL[] urls = new URL[]{
new URL("file:D:\\\\")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class clazz = urlClassLoader.loadClass("com.google.$Proxy");
Constructor constructor = clazz.getConstructor(targetInf);
proxy = constructor.newInstance(target);
}catch (Exception e){
e.printStackTrace();
}
//返回生成的代理对象
return proxy;
}
}
JDK提供的Proxy,原理上与上面大同小异。通过接口反射得到字节码(Proxy.generateProxyClass()),然后把字节码转成class。(通过native方法,defineClass()。)
常见的一道面试题,下面代码为何出错?
如果想要弄明白为什么会出错,就首先要明白,JDK是如何代理的?
将JDK代理生成器,生成的代理类字节码输出
查看字节码
发现这个JDK代理生成是需要继承Proxy,那么根据JAVA语言特性,就无法像Cglib继承实现类,所以,代理类和被代理类是没有关系的,当我们getBean自然也不是被代理类。这个原理也正是解释为什么连接点this在不同的代理模式下,出现不同的结果。
结论:在JDK动态代理中使用的是聚合而不是继承,因为代理对象需要继承Proxy,所以无法继承目标对象。