Type接口与Class类的区别联系

现在来补充下Java中Type接口与Class类的区别联系。

  • Type是Class的父接口。
  • Class是Type的子类。

提示:因为AOP是基于动态代理生成,如果想要仔细研究生成的代理类长什么样,可以设置系统参数-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,这样就会保存所有自动生成的代理类(注:生产环境严禁使用)。

  1. package java.lang.reflect;
  2. /**
  3. * Type is the common superinterface for all types in the Java
  4. * programming language. These include raw types, parameterized types,
  5. * array types, type variables and primitive types.
  6. *
  7. * @since 1.5
  8. */
  9. public interface Type {
  10. /**
  11. * Returns a string describing this type, including information
  12. * about any type parameters.
  13. *
  14. * @implSpec The default implementation calls {@code toString}.
  15. *
  16. * @return a string describing this type
  17. * @since 1.8
  18. */
  19. default String getTypeName() {
  20. return toString();
  21. }
  22. }

其主要的子类包括:

Spring AOP 通知参数获取 - 图1

总结来说:

  • 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 {

  1. @Pointcut(value="execution(* bean.HelloApi.aspectTest(..)) && args(a1,b2)",argNames="a1,b2")
  2. public void pointcut1(String a1,String b2){}
  3. @Before(value="pointcut1(a,b)",argNames="a,b")
  4. public void beforecase1(String a,String b){
  5. System.out.println("1 a:" + a +" b:" + b);
  6. }
  7. //注意和beforecase1的区别是argNames的顺序交换了
  8. @Before(value="pointcut1(a,b)",argNames="b,a")
  9. public void beforecase2(String a,String b){
  10. System.out.println("2 a:" + a +" b:" + b);
  11. }

}

  1. ```java
  2. package bean;
  3. public class HelloApi {
  4. public void aspectTest(String a,String b){
  5. System.out.println("in aspectTest:" + "a:" + a + ",b:" + b);
  6. }
  7. }

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。解决了目标方法参数传入到切入方法参数的问题。

但是,如果设置了argNamesSpring不再使用方法参数的名字来配对,使用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文件中含有变量调试信息”将使用这些方法签名中的参数名来确定参数名;

  1. @Before(value=" args(param)") //不需要argNames了
  2. public void before1(JoinPoint jp, String param) {
  3. System.out.println("===param:" + param);
  4. }

如果没有“class文件中含有变量调试信息”,将尝试自己的参数匹配算法,如果发现参数绑定有二义性将抛出AmbiguousBindingException异常;对于只有一个绑定变量的切入点表达式,而通知方法只接受一个参数,说明绑定参数是明确的,从而能配对成功。

  1. @Before(value=" args(param)")
  2. public void before1(JoinPoint jp, String param) {
  3. System.out.println("===param:" + param);
  4. }

以上策略失败将抛出IllegalArgumentException

PS:【class文件中生成变量调试信息】在myeclipse中打开windows-》preferences,设置如下:

Spring AOP 通知参数获取 - 图2

手动获取参数

现在AOP的场景越来越多,所以我们有必要理解下和AOP相关的一些概念和机制。基础知识和原理类大家搜索spring aop/aspectj,有大量现成的可以参考,基本上只要理解了jdk动态代理、cglib字节码动态生成代理就足够了,而且必须知道这个代理类是spring托管的(如果是自己创建的代理类,是无法被拦截的,此时只能使用过滤器/拦截器机制,他们本身是链式的,跟代理无关),所以这里就不重复废话了。

ProceedingJoinPoint和JoinPoint(获取各对象备忘)

想要获取被切方法返回类型、参数名称/值,得先了解

  1. JoinPoint:提供访问当前被通知方法的目标对象、代理对象、方法参数等数据
  2. ProceedingJoinPoint:用于环绕通知,使用proceed()方法来执行目标方法
  3. JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等:
  1. import org.aspectj.lang.reflect.SourceLocation;
  2. public interface JoinPoint {
  3. String toString(); //连接点所在位置的相关信息
  4. String toShortString(); //连接点所在位置的简短相关信息
  5. String toLongString(); //连接点所在位置的全部相关信息
  6. Object getThis(); //返回AOP代理对象,也就是com.sun.proxy.$Proxy18
  7. Object getTarget(); //返回目标对象,一般我们都需要它或者(也就是定义方法的接口或类,为什么会是接口呢?这主要是在目标对象本身是动态代理的情况下,例如Mapper。所以返回的是定义方法的对象如aoptest.daoimpl.GoodDaoImpl或com.b.base.BaseMapper<T, E, PK>)
  8. Object[] getArgs(); //返回被通知方法参数列表
  9. Signature getSignature(); //返回当前连接点签名 其getName()方法返回方法的FQN,如void aoptest.dao.GoodDao.delete()或com.b.base.BaseMapper.insert(T)(需要注意的是,很多时候我们定义了子类继承父类的时候,我们希望拿到基于子类的FQN,这直接可拿不到,要依赖于AopUtils.getTargetClass(point.getTarget())获取原始代理对象,下面会详细讲解)
  10. SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置
  11. String getKind(); //连接点类型
  12. StaticPart getStaticPart(); //返回连接点静态部分
  13. }
  14. public interface ProceedingJoinPoint extends JoinPoint {
  15. public Object proceed() throws Throwable;
  16. public Object proceed(Object[] args) throws Throwable;
  17. }

JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等:

  1. public interface StaticPart {
  2. Signature getSignature(); //返回当前连接点签名
  3. String getKind(); //连接点类型
  4. int getId(); //唯一标识
  5. String toString(); //连接点所在位置的相关信息
  6. String toShortString(); //连接点所在位置的简短相关信息
  7. String toLongString(); //连接点所在位置的全部相关信息
  8. }

环绕通知 ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行,这也是环绕通知和前置、后置通知方法的一个最大区别。

Proceedingjoinpoint 继承了 JoinPoint 。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。

暴露出这个方法,就能支持 aop:around这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。

被切方法注解

  1. public Object around(ProceedingJoinPoint point) throws Throwable {
  2. Signature signature = point.getSignature();
  3. // getGenericInterfaces方法能够获取类/接口实现的所有接口
  4. Type[] types = AopUtils.getTargetClass(point.getTarget()).getGenericInterfaces();
  5. Annotation nologgingAnno = ((Class)types[0]).getAnnotation(Nologging.class); // type是所有类型的父接口
  6. MethodSignature methodSignature = (MethodSignature)signature;
  7. Method targetMethod = methodSignature.getMethod();
  8. }

AopUtils.getTargetClass(point.getTarget())获取原始对象。
例如对于Mapper而言,它获取的是具体代理的Mapper如com.b.mapper.DefaultDsMapper(如果前者继承了后者的话)
而不是定义该方法的Mapper如com.b.base.BaseMapper<Info, InfoExample, InfoKey>,如下图
Spring AOP 通知参数获取 - 图3
获取注解后使用如下方式获取注解中的参数:

  1. package com.haha.study.annotation.value;
  2. import java.lang.annotation.Documented;
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7. /**
  8. * description: 定义一个注解。
  9. * @version v1.0
  10. * @author w
  11. * @date 2018年8月1日下午2:41:45
  12. */
  13. @Documented
  14. @Retention(RetentionPolicy.RUNTIME)
  15. @Target(ElementType.TYPE)
  16. public @interface ConsAnnotation {
  17. String[] request();
  18. }
  1. package com.haha.study.annotation.value;
  2. /**
  3. * description: 创建一个普通的类,使用 @ConsAnnotation、@Fields 注解。
  4. * @version v1.0
  5. * @author w
  6. * @date 2018年8月1日下午2:50:23
  7. */
  8. @ConsAnnotation(request = { "hello","world","annotation!" })
  9. public class User {
  10. @Fields("中华人民共和国")
  11. private String userName;
  12. public String getUserName() {
  13. return userName;
  14. }
  15. public void setUserName(String userName) {
  16. this.userName = userName;
  17. }
  18. }
  1. package com.haha.study.annotation.value;
  2. import java.lang.reflect.Field;
  3. import java.util.Arrays;
  4. /**
  5. * description: 针对 com.haha.study.annotation.value.User 类使用注解的测试
  6. * @version v1.0
  7. * @author w
  8. * @date 2018年8月1日下午2:37:13
  9. */
  10. public class ValueTest {
  11. public static void main(String[] args) throws Exception {
  12. User user = new User();
  13. // 1、 获取 User类上的注解 @ConsAnnotation
  14. ConsAnnotation anno = user.getClass().getAnnotation(ConsAnnotation.class);
  15. String[] arr = anno.request();
  16. System.out.println(Arrays.toString(arr)); // [hello, world, annotation!]
  17. // 2、 获取User类中 private String userName; 变量上的注解 @Field
  18. Field f = user.getClass().getDeclaredField("userName");
  19. Fields anno2 = f.getAnnotation(Fields.class);
  20. user.setUserName(anno2.value());
  21. System.out.println(user.getUserName()); // 中华人民共和国
  22. }
  23. }

通知方法参数

JoinPoint.getArgs() 获取到被通知方法参数

  1. Object[] args = joinPoint.getArgs();

使用如下方式在通知方法上声明,必须是在第一个参数,然后使用jp.getArgs()就能获取到被通知方法参数

  1. @Before(value="execution(* sayBefore(*))")
  2. public void before(JoinPoint jp) {}
  3. @Before(value="execution(* sayBefore(*))")
  4. public void before(JoinPoint.StaticPart jp) {}

通知方法参数名称

  1. Signature signature = joinPoint.getSignature();
  2. if (signature instanceof MethodSignature) {
  3. MethodSignature methodSignature = (MethodSignature) signature;
  4. String[] properties = methodSignature.getParameterNames();
  5. }

通知方法返回类型

  1. Signature signature = joinPoint.getSignature();
  2. if (signature instanceof MethodSignature) {
  3. MethodSignature methodSignature = (MethodSignature) signature;
  4. // 被切的方法
  5. Method method = methodSignature.getMethod();
  6. // 返回类型
  7. Class<?> methodReturnType = method.getReturnType();
  8. // 实例化
  9. Object o = methodReturnType.newInstance();
  10. }

通知方法全限定类名

  1. Signature signature = joinPoint.getSignature();
  2. signature.getDeclaringTypeName()

通知方法名

  1. Signature signature = joinPoint.getSignature();
  2. signature.getName()

工具类方法

  1. /**
  2. * 获取方法和参数内容
  3. *
  4. * @param className 类名 非必填
  5. * @param mothodName 方法名 必填
  6. * @param properties 参数名称列表
  7. * @param agrs 参数列表
  8. * @return
  9. */
  10. public static String getMothodPropertion(String className, String mothodName, String[] properties, Object ... agrs) {
  11. StringBuilder sb = new StringBuilder();
  12. if(null != className) {
  13. sb.append("类名:[").append(className).append("],");
  14. }
  15. sb.append("方法:[").append(mothodName).append("]");
  16. if (null == agrs) {
  17. return sb.toString();
  18. }
  19. boolean flag = null != properties;
  20. int propertiesLength = properties.length;
  21. Object obj;
  22. for(int i = 0, length = agrs.length; i < length; i++) {
  23. obj = agrs[i];
  24. sb.append("\r\n参数索引:[").append(i).append("],");
  25. if (flag && i < propertiesLength) {
  26. sb.append("参数名称:[").append(properties[i]).append("],");
  27. }
  28. if(null == obj) {
  29. sb.append("为null");
  30. continue;
  31. }
  32. sb.append("类型:[").append(obj.getClass().getName()).append("],");
  33. if(obj instanceof Collection) {
  34. Collection collection = (Collection)obj;
  35. sb.append("长度:[").append(collection.size()).append("],内容:[").append(collection);
  36. } else if(obj instanceof Map) {
  37. Map map = (Map)obj;
  38. sb.append("长度:[").append(map.size()).append("],内容:[").append(map);
  39. } else if(obj instanceof Object[]) {
  40. Object[] objects = (Object[])obj;
  41. sb.append("长度:[").append(objects.length).append("],内容:[").append(Arrays.asList(objects));
  42. } else if(obj instanceof String) {
  43. sb.append("内容:[").append(obj);
  44. } else {
  45. sb.append("内容:[").append(String.valueOf(obj));
  46. }
  47. sb.append("]");
  48. }
  49. return sb.toString();
  50. }

效果如下

image.png

自动获取

通过切入点表达式可以将相应的参数自动传递给通知方法,例如前边章节讲过的返回值和异常是如何传递给通知方法的。

被切方法里的变量

Spring AOP中,除了executionbean指示符不能传递参数给通知方法,其他指示符都可以将匹配的相应参数或对象自动传递给通知方法。

  1. @Before(value="execution(* test(*)) && args(param)", argNames="param")
  2. public void before1(String param) {
  3. System.out.println("===param:" + param);
  4. }

切入点表达式execution(* test(*)) && args(param)

  1. 首先execution( test(*))匹配任何方法名为test,且有一个任何类型的参数;
  2. args(param)将首先查找通知方法上同名的参数,并在方法执行时(运行时)匹配传入的参数是使用该同名参数类型,即java.lang.String;如果匹配将把该被通知参数传递给通知方法上同名参数。

其他指示符(除了executionbean指示符)都可以使用这种方式进行参数绑定。

我们可能想获取被切方法里的某个变量(方法可能有多个变量):

  1. @Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" +
  2. "args(account,..)")
  3. private void accountDataAccessOperation(Account account) {}

注意到 agrs中的account后面还有 ..,对于被切的方法参数中包含不只account一个来讲很关键。

在此有一个问题: 在class文件中没生成变量调试信息是获取不到方法参数名字的。

所以我们可以使用策略来确定参数名:如果我们通过argNames属性指定了参数名,那么就是要我们指定的;

  1. @Before(value=" args(param)", argNames="param") //明确指定了
  2. public void before1(String param) {
  3. System.out.println("===param:" + param);
  4. }

如果第一个参数类型是JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart类型,可以从argNames属性中省略掉该参数名(但是最好写上),这些类型对象会自动传入的,但必须作为第一个参数;

  1. @Before(value=" args(param)", argNames="param") //明确指定了
  2. public void before1(JoinPoint jp, String param) {
  3. System.out.println("===param:" + param);
  4. }

被切方法的注解

要获取自定义注解参数,就需要在自定义注解中增加几个属性,下面自定义的TestAnnotation中有两个属性:value和description。

  1. package cn.ganlixin.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @Target(ElementType.METHOD)
  8. public @interface TestAnnotation {
  9. String value();
  10. String description() default "default description";
  11. }

创建Aspect

  1. package cn.ganlixin.aspect;
  2. import cn.ganlixin.annotation.TestAnnotation;
  3. import org.aspectj.lang.annotation.Aspect;
  4. import org.aspectj.lang.annotation.Before;
  5. import org.aspectj.lang.annotation.Pointcut;
  6. import org.springframework.stereotype.Component;
  7. @Component
  8. @Aspect
  9. public class TestAspect {
  10. // 先创建一个方法,方法名随意,但是需要制定@annotation为刚刚自定义的注解
  11. @Pointcut("@annotation(cn.ganlixin.annotation.TestAnnotation)")
  12. public void test() {}
  13. // 使用@Before,需要先引入上面@Pointcut注解的方法名,在加上@annotation,
  14. // @annotation中的值,需要和action方法中的参数名称相同(必须相同,但是名称任意)
  15. @Before("test() && @annotation(testAnnotation)")
  16. public void action(TestAnnotation testAnnotation) {
  17. System.out.println("Annotation value : " + testAnnotation.value());
  18. System.out.println("Annotation description : " + testAnnotation.description());
  19. System.out.println("this is TestAspect.action()");
  20. }
  21. }

划重点:

  1. // 第2个示例,强调@annotation中的值,需要和方法参数名相同
  2. @Before("test() && @annotation(abcdef)")
  3. public void action2(TestAnnotation abcdef) {
  4. System.out.println("Annotation value : " + abcdef.value());
  5. System.out.println("Annotation description : " + abcdef.description());
  6. System.out.println("this is TestAspect.action()");
  7. }

当然,你也可以使用argNames指定参数位置

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface OpLog {
  4. /**
  5. *
  6. * 操作类型
  7. */
  8. int opModule();
  9. /**
  10. *
  11. * 是否批量
  12. */
  13. String batch();
  14. /**
  15. *
  16. * 操作来源(页面+操作)
  17. */
  18. String source();
  19. }
  1. @Aspect
  2. @Slf4j
  3. public class OpLogAspectj {
  4. @Pointcut(value = "@annotation(com.noob.annotation.OpLog)")
  5. public void methodPointcut(){}
  6. /**
  7. * 对带注解@OpLog的方法进行切面,并获取到注解的属性值
  8. */
  9. @Around(value= "methodPointcut() && @annotation(opLog)" , argNames="opLog")
  10. public Object around(ProceedingJoinPoint point, OpLog opLog) throws Throwable{
  11. Object obj = null;
  12. Object[] args = point.getArgs();
  13. try {
  14. obj = point.proceed(args);
  15. } catch (Throwable e) {
  16. log.error("方法执行异常", e);
  17. }
  18. long endTime = System.currentTimeMillis();
  19. MethodSignature signature = (MethodSignature) point.getSignature();
  20. String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
  21. return obj;
  22. }
  23. @Pointcut(value = "@annotation(org.apache.shiro.authz.annotation.RequiresPermissions)")
  24. public void methodPointcut2(){}
  25. /**
  26. * 配置指定位置指定类型的参数
  27. */
  28. @Around(value= "methodPointcut2() && (args(request, ..) || args(.., request))")
  29. public Object around2(ProceedingJoinPoint point, HttpServletRequest request) throws Throwable{
  30. Object obj = null;
  31. Object[] args = point.getArgs();
  32. try {
  33. SsoUser ssoUser = SsoSession.getCurrentUser(request);
  34. obj = point.proceed(args);
  35. } catch (Throwable e) {
  36. log.error("方法执行异常", e);
  37. }
  38. return obj;
  39. }
  40. }

测试

  1. package cn.ganlixin.service;
  2. import cn.ganlixin.annotation.TestAnnotation;
  3. import org.springframework.stereotype.Service;
  4. @Service
  5. public class TestService {
  6. @TestAnnotation("this is value")
  7. public void test1() {
  8. System.out.println("this is TestService.test1()");
  9. }
  10. @TestAnnotation(value = "this is another value", description = "this is description")
  11. public void test2() {
  12. System.out.println("this is TestService.test2()");
  13. }
  14. }
  1. package cn.ganlixin;
  2. import cn.ganlixin.service.TestService;
  3. import org.junit.Test;
  4. import org.springframework.context.ApplicationContext;
  5. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  6. public class AppTest {
  7. @Test
  8. public void testAOP2() {
  9. ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
  10. TestService testService = context.getBean(TestService.class);
  11. testService.test1();
  12. System.out.println("----------------------------");
  13. testService.test2();
  14. }
  15. }

输出:

  1. Annotation value : this is value
  2. Annotation description : default description
  3. this is TestAspect.action()
  4. this is TestService.test1()
  5. ----------------------------
  6. Annotation value : this is another value
  7. Annotation description : this is description
  8. this is TestAspect.action()
  9. this is TestService.test2()

总结:要想是获取AOP中自定义注解的参数值,主要就一点:

Spring AOP 通知参数获取 - 图5

组合例子

接下来让我们示例一下组合情况吧:

  1. @Before(args(param) && target(bean) && @annotation(secure)",
  2. argNames="jp,param,bean,secure")
  3. public void before5(JoinPoint jp, String param,
  4. IPointcutService pointcutService, Secure secure) {
  5. ……
  6. }

也可以对使用命名切入点自动获取参数:

  1. @Pointcut(value="args(param)", argNames="param")
  2. private void pointcut1(String param){}
  3. @Pointcut(value="@annotation(secure)", argNames="secure")
  4. private void pointcut2(Secure secure){}
  5. @Before(value = "pointcut1(param) && pointcut2(secure)", argNames="param, secure")
  6. public void before6(JoinPoint jp, String param, Secure secure) {
  7. ……
  8. }

参考文献

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