Type接口与Class类的区别联系
现在来补充下Java中Type接口与Class类的区别联系。
- Type是Class的父接口。
- Class是Type的子类。
提示:因为AOP是基于动态代理生成,如果想要仔细研究生成的代理类长什么样,可以设置系统参数-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,这样就会保存所有自动生成的代理类(注:生产环境严禁使用)。
package java.lang.reflect;/*** Type is the common superinterface for all types in the Java* programming language. These include raw types, parameterized types,* array types, type variables and primitive types.** @since 1.5*/public interface Type {/*** Returns a string describing this type, including information* about any type parameters.** @implSpec The default implementation calls {@code toString}.** @return a string describing this type* @since 1.8*/default String getTypeName() {return toString();}}
其主要的子类包括:

总结来说:
- Type是一个接口。
- Type是Java中所有类型的父接口,有一些子类,如上所示。
- Type包括:raw type(原始类型,对应Class),parameterized types(参数化类型), array types(数组类型), type variables(类型变量) and primitive types(基本类型,对应Class).
- Type是JDK1.5引入的,主要是为了泛型。
Spring中argNames的含义
```java package aspect;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component;
import java.lang.String;
@Component @Aspect public class HelloApiAspect2 {
@Pointcut(value="execution(* bean.HelloApi.aspectTest(..)) && args(a1,b2)",argNames="a1,b2")public void pointcut1(String a1,String b2){}@Before(value="pointcut1(a,b)",argNames="a,b")public void beforecase1(String a,String b){System.out.println("1 a:" + a +" b:" + b);}//注意和beforecase1的区别是argNames的顺序交换了@Before(value="pointcut1(a,b)",argNames="b,a")public void beforecase2(String a,String b){System.out.println("2 a:" + a +" b:" + b);}
}
```javapackage bean;public class HelloApi {public void aspectTest(String a,String b){System.out.println("in aspectTest:" + "a:" + a + ",b:" + b);}}
args(a1,b2)另外一个作用,就是定义了aspectTest(String a,String b)方法对应表达式args(a1,b2)。定义了args(a1,b2),才能把目标方法aspectTest的参数传入到切面方法beforecase1的参数中,a参数对应a1,b参数对应b2。使用的方法是按顺序一一对应,aspectTest第一个参数对args第一个参数,aspectTest第2个参数对args第2个参数.
argNames是可选的,如果没有argNames这个参数,而编译器设置了【在class文件生成变量调试信息】,则spring可以通过反射知道方法参数的名字,通过名字配对,Spring知道args(a1,b2)表达式里面的a1和b2,对应了pointcut1(String a1,String b2)方法里面的a1和b2。
目标方法和切入方法的参数的关系是这样确立的:aspectTest(String a,String b) 与 args(a1,b2)关系是a对a1,b对b2,args(a1,b2)与pointcut1(String a1,String b2)关系是args的a1对pointcut1的a1,args的a2对pointcut1的a2。解决了目标方法参数传入到切入方法参数的问题。
但是,如果设置了argNames,Spring不再使用方法参数的名字来配对,使用argNames定义的顺序来定义pointcut1(String a1,String b2)的顺序,例如:argNames="a1,b2",a1在b2前面,表示pointcut1方法第一个参数是a1,第二个参数是b2。
既然不设置argNames,Spring可以根据参数名字进行配对,为什么还需要配置argNames?
因为Spring要知道方法的参数名,编译器必须设置了【在class文件生成变量调试信息】,如果没有设置,Spring就不知道pointcut1方法的参数名了,这个时候,Spring只知道参数的类型,Spring会使用参数的类型进行配对,如果出现2个参数都是同一个类型的情况,就会报AmbiguousBindingException异常:
如果“class文件中含有变量调试信息”将使用这些方法签名中的参数名来确定参数名;
@Before(value=" args(param)") //不需要argNames了public void before1(JoinPoint jp, String param) {System.out.println("===param:" + param);}
如果没有“class文件中含有变量调试信息”,将尝试自己的参数匹配算法,如果发现参数绑定有二义性将抛出AmbiguousBindingException异常;对于只有一个绑定变量的切入点表达式,而通知方法只接受一个参数,说明绑定参数是明确的,从而能配对成功。
@Before(value=" args(param)")public void before1(JoinPoint jp, String param) {System.out.println("===param:" + param);}
以上策略失败将抛出IllegalArgumentException。
PS:【class文件中生成变量调试信息】在myeclipse中打开windows-》preferences,设置如下:
手动获取参数
现在AOP的场景越来越多,所以我们有必要理解下和AOP相关的一些概念和机制。基础知识和原理类大家搜索spring aop/aspectj,有大量现成的可以参考,基本上只要理解了jdk动态代理、cglib字节码动态生成代理就足够了,而且必须知道这个代理类是spring托管的(如果是自己创建的代理类,是无法被拦截的,此时只能使用过滤器/拦截器机制,他们本身是链式的,跟代理无关),所以这里就不重复废话了。
ProceedingJoinPoint和JoinPoint(获取各对象备忘)
想要获取被切方法返回类型、参数名称/值,得先了解
- JoinPoint:提供访问当前被通知方法的目标对象、代理对象、方法参数等数据
- ProceedingJoinPoint:用于环绕通知,使用
proceed()方法来执行目标方法 - JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等:
import org.aspectj.lang.reflect.SourceLocation;public interface JoinPoint {String toString(); //连接点所在位置的相关信息String toShortString(); //连接点所在位置的简短相关信息String toLongString(); //连接点所在位置的全部相关信息Object getThis(); //返回AOP代理对象,也就是com.sun.proxy.$Proxy18Object getTarget(); //返回目标对象,一般我们都需要它或者(也就是定义方法的接口或类,为什么会是接口呢?这主要是在目标对象本身是动态代理的情况下,例如Mapper。所以返回的是定义方法的对象如aoptest.daoimpl.GoodDaoImpl或com.b.base.BaseMapper<T, E, PK>)Object[] getArgs(); //返回被通知方法参数列表Signature getSignature(); //返回当前连接点签名 其getName()方法返回方法的FQN,如void aoptest.dao.GoodDao.delete()或com.b.base.BaseMapper.insert(T)(需要注意的是,很多时候我们定义了子类继承父类的时候,我们希望拿到基于子类的FQN,这直接可拿不到,要依赖于AopUtils.getTargetClass(point.getTarget())获取原始代理对象,下面会详细讲解)SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置String getKind(); //连接点类型StaticPart getStaticPart(); //返回连接点静态部分}public interface ProceedingJoinPoint extends JoinPoint {public Object proceed() throws Throwable;public Object proceed(Object[] args) throws Throwable;}
JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等:
public interface StaticPart {Signature getSignature(); //返回当前连接点签名String getKind(); //连接点类型int getId(); //唯一标识String toString(); //连接点所在位置的相关信息String toShortString(); //连接点所在位置的简短相关信息String toLongString(); //连接点所在位置的全部相关信息}
环绕通知 ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行,这也是环绕通知和前置、后置通知方法的一个最大区别。
Proceedingjoinpoint 继承了 JoinPoint 。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
暴露出这个方法,就能支持 aop:around这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。
被切方法注解
public Object around(ProceedingJoinPoint point) throws Throwable {Signature signature = point.getSignature();// getGenericInterfaces方法能够获取类/接口实现的所有接口Type[] types = AopUtils.getTargetClass(point.getTarget()).getGenericInterfaces();Annotation nologgingAnno = ((Class)types[0]).getAnnotation(Nologging.class); // type是所有类型的父接口MethodSignature methodSignature = (MethodSignature)signature;Method targetMethod = methodSignature.getMethod();}
AopUtils.getTargetClass(point.getTarget())获取原始对象。
例如对于Mapper而言,它获取的是具体代理的Mapper如com.b.mapper.DefaultDsMapper(如果前者继承了后者的话)
而不是定义该方法的Mapper如com.b.base.BaseMapper<Info, InfoExample, InfoKey>,如下图
获取注解后使用如下方式获取注解中的参数:
package com.haha.study.annotation.value;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/*** description: 定义一个注解。* @version v1.0* @author w* @date 2018年8月1日下午2:41:45*/@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface ConsAnnotation {String[] request();}
package com.haha.study.annotation.value;/*** description: 创建一个普通的类,使用 @ConsAnnotation、@Fields 注解。* @version v1.0* @author w* @date 2018年8月1日下午2:50:23*/@ConsAnnotation(request = { "hello","world","annotation!" })public class User {@Fields("中华人民共和国")private String userName;public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}}
package com.haha.study.annotation.value;import java.lang.reflect.Field;import java.util.Arrays;/*** description: 针对 com.haha.study.annotation.value.User 类使用注解的测试* @version v1.0* @author w* @date 2018年8月1日下午2:37:13*/public class ValueTest {public static void main(String[] args) throws Exception {User user = new User();// 1、 获取 User类上的注解 @ConsAnnotationConsAnnotation anno = user.getClass().getAnnotation(ConsAnnotation.class);String[] arr = anno.request();System.out.println(Arrays.toString(arr)); // [hello, world, annotation!]// 2、 获取User类中 private String userName; 变量上的注解 @FieldField f = user.getClass().getDeclaredField("userName");Fields anno2 = f.getAnnotation(Fields.class);user.setUserName(anno2.value());System.out.println(user.getUserName()); // 中华人民共和国}}
通知方法参数
JoinPoint.getArgs() 获取到被通知方法参数
Object[] args = joinPoint.getArgs();
使用如下方式在通知方法上声明,必须是在第一个参数,然后使用jp.getArgs()就能获取到被通知方法参数
@Before(value="execution(* sayBefore(*))")public void before(JoinPoint jp) {}@Before(value="execution(* sayBefore(*))")public void before(JoinPoint.StaticPart jp) {}
通知方法参数名称
Signature signature = joinPoint.getSignature();if (signature instanceof MethodSignature) {MethodSignature methodSignature = (MethodSignature) signature;String[] properties = methodSignature.getParameterNames();}
通知方法返回类型
Signature signature = joinPoint.getSignature();if (signature instanceof MethodSignature) {MethodSignature methodSignature = (MethodSignature) signature;// 被切的方法Method method = methodSignature.getMethod();// 返回类型Class<?> methodReturnType = method.getReturnType();// 实例化Object o = methodReturnType.newInstance();}
通知方法全限定类名
Signature signature = joinPoint.getSignature();signature.getDeclaringTypeName()
通知方法名
Signature signature = joinPoint.getSignature();signature.getName()
工具类方法
/*** 获取方法和参数内容** @param className 类名 非必填* @param mothodName 方法名 必填* @param properties 参数名称列表* @param agrs 参数列表* @return*/public static String getMothodPropertion(String className, String mothodName, String[] properties, Object ... agrs) {StringBuilder sb = new StringBuilder();if(null != className) {sb.append("类名:[").append(className).append("],");}sb.append("方法:[").append(mothodName).append("]");if (null == agrs) {return sb.toString();}boolean flag = null != properties;int propertiesLength = properties.length;Object obj;for(int i = 0, length = agrs.length; i < length; i++) {obj = agrs[i];sb.append("\r\n参数索引:[").append(i).append("],");if (flag && i < propertiesLength) {sb.append("参数名称:[").append(properties[i]).append("],");}if(null == obj) {sb.append("为null");continue;}sb.append("类型:[").append(obj.getClass().getName()).append("],");if(obj instanceof Collection) {Collection collection = (Collection)obj;sb.append("长度:[").append(collection.size()).append("],内容:[").append(collection);} else if(obj instanceof Map) {Map map = (Map)obj;sb.append("长度:[").append(map.size()).append("],内容:[").append(map);} else if(obj instanceof Object[]) {Object[] objects = (Object[])obj;sb.append("长度:[").append(objects.length).append("],内容:[").append(Arrays.asList(objects));} else if(obj instanceof String) {sb.append("内容:[").append(obj);} else {sb.append("内容:[").append(String.valueOf(obj));}sb.append("]");}return sb.toString();}
效果如下
自动获取
通过切入点表达式可以将相应的参数自动传递给通知方法,例如前边章节讲过的返回值和异常是如何传递给通知方法的。
被切方法里的变量
在Spring AOP中,除了execution和bean指示符不能传递参数给通知方法,其他指示符都可以将匹配的相应参数或对象自动传递给通知方法。
@Before(value="execution(* test(*)) && args(param)", argNames="param")public void before1(String param) {System.out.println("===param:" + param);}
切入点表达式execution(* test(*)) && args(param):
- 首先
execution( test(*))匹配任何方法名为test,且有一个任何类型的参数; args(param)将首先查找通知方法上同名的参数,并在方法执行时(运行时)匹配传入的参数是使用该同名参数类型,即java.lang.String;如果匹配将把该被通知参数传递给通知方法上同名参数。
其他指示符(除了execution和bean指示符)都可以使用这种方式进行参数绑定。
我们可能想获取被切方法里的某个变量(方法可能有多个变量):
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" +"args(account,..)")private void accountDataAccessOperation(Account account) {}
注意到 agrs中的account后面还有 ..,对于被切的方法参数中包含不只account一个来讲很关键。
在此有一个问题: 在class文件中没生成变量调试信息是获取不到方法参数名字的。
所以我们可以使用策略来确定参数名:如果我们通过argNames属性指定了参数名,那么就是要我们指定的;
@Before(value=" args(param)", argNames="param") //明确指定了public void before1(String param) {System.out.println("===param:" + param);}
如果第一个参数类型是JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart类型,可以从argNames属性中省略掉该参数名(但是最好写上),这些类型对象会自动传入的,但必须作为第一个参数;
@Before(value=" args(param)", argNames="param") //明确指定了public void before1(JoinPoint jp, String param) {System.out.println("===param:" + param);}
被切方法的注解
要获取自定义注解参数,就需要在自定义注解中增加几个属性,下面自定义的TestAnnotation中有两个属性:value和description。
package cn.ganlixin.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface TestAnnotation {String value();String description() default "default description";}
创建Aspect
package cn.ganlixin.aspect;import cn.ganlixin.annotation.TestAnnotation;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Component@Aspectpublic class TestAspect {// 先创建一个方法,方法名随意,但是需要制定@annotation为刚刚自定义的注解@Pointcut("@annotation(cn.ganlixin.annotation.TestAnnotation)")public void test() {}// 使用@Before,需要先引入上面@Pointcut注解的方法名,在加上@annotation,// @annotation中的值,需要和action方法中的参数名称相同(必须相同,但是名称任意)@Before("test() && @annotation(testAnnotation)")public void action(TestAnnotation testAnnotation) {System.out.println("Annotation value : " + testAnnotation.value());System.out.println("Annotation description : " + testAnnotation.description());System.out.println("this is TestAspect.action()");}}
划重点:
// 第2个示例,强调@annotation中的值,需要和方法参数名相同@Before("test() && @annotation(abcdef)")public void action2(TestAnnotation abcdef) {System.out.println("Annotation value : " + abcdef.value());System.out.println("Annotation description : " + abcdef.description());System.out.println("this is TestAspect.action()");}
当然,你也可以使用argNames指定参数位置
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface OpLog {/**** 操作类型*/int opModule();/**** 是否批量*/String batch();/**** 操作来源(页面+操作)*/String source();}
@Aspect@Slf4jpublic class OpLogAspectj {@Pointcut(value = "@annotation(com.noob.annotation.OpLog)")public void methodPointcut(){}/*** 对带注解@OpLog的方法进行切面,并获取到注解的属性值*/@Around(value= "methodPointcut() && @annotation(opLog)" , argNames="opLog")public Object around(ProceedingJoinPoint point, OpLog opLog) throws Throwable{Object obj = null;Object[] args = point.getArgs();try {obj = point.proceed(args);} catch (Throwable e) {log.error("方法执行异常", e);}long endTime = System.currentTimeMillis();MethodSignature signature = (MethodSignature) point.getSignature();String methodName = signature.getDeclaringTypeName() + "." + signature.getName();return obj;}@Pointcut(value = "@annotation(org.apache.shiro.authz.annotation.RequiresPermissions)")public void methodPointcut2(){}/*** 配置指定位置指定类型的参数*/@Around(value= "methodPointcut2() && (args(request, ..) || args(.., request))")public Object around2(ProceedingJoinPoint point, HttpServletRequest request) throws Throwable{Object obj = null;Object[] args = point.getArgs();try {SsoUser ssoUser = SsoSession.getCurrentUser(request);obj = point.proceed(args);} catch (Throwable e) {log.error("方法执行异常", e);}return obj;}}
测试
package cn.ganlixin.service;import cn.ganlixin.annotation.TestAnnotation;import org.springframework.stereotype.Service;@Servicepublic class TestService {@TestAnnotation("this is value")public void test1() {System.out.println("this is TestService.test1()");}@TestAnnotation(value = "this is another value", description = "this is description")public void test2() {System.out.println("this is TestService.test2()");}}
package cn.ganlixin;import cn.ganlixin.service.TestService;import org.junit.Test;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class AppTest {@Testpublic void testAOP2() {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);TestService testService = context.getBean(TestService.class);testService.test1();System.out.println("----------------------------");testService.test2();}}
输出:
Annotation value : this is valueAnnotation description : default descriptionthis is TestAspect.action()this is TestService.test1()----------------------------Annotation value : this is another valueAnnotation description : this is descriptionthis is TestAspect.action()this is TestService.test2()
总结:要想是获取AOP中自定义注解的参数值,主要就一点:
组合例子
接下来让我们示例一下组合情况吧:
@Before(args(param) && target(bean) && @annotation(secure)",argNames="jp,param,bean,secure")public void before5(JoinPoint jp, String param,IPointcutService pointcutService, Secure secure) {……}
也可以对使用命名切入点自动获取参数:
@Pointcut(value="args(param)", argNames="param")private void pointcut1(String param){}@Pointcut(value="@annotation(secure)", argNames="secure")private void pointcut2(Secure secure){}@Before(value = "pointcut1(param) && pointcut2(secure)", argNames="param, secure")public void before6(JoinPoint jp, String param, Secure secure) {……}
参考文献
AOP注解参数:
https://www.cnblogs.com/-beyond/p/11387487.html https://blog.csdn.net/fajing_feiyue/article/details/100068726 https://www.cnblogs.com/keeya/p/9952700.html https://blog.csdn.net/weixin_33676492/article/details/92388130 https://blog.csdn.net/HaHa_Sir/article/details/81331437
