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);
}
}
```java
package 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.$Proxy18
Object 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类上的注解 @ConsAnnotation
ConsAnnotation anno = user.getClass().getAnnotation(ConsAnnotation.class);
String[] arr = anno.request();
System.out.println(Arrays.toString(arr)); // [hello, world, annotation!]
// 2、 获取User类中 private String userName; 变量上的注解 @Field
Field 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
@Aspect
public 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
@Slf4j
public 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;
@Service
public 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 {
@Test
public 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 value
Annotation description : default description
this is TestAspect.action()
this is TestService.test1()
----------------------------
Annotation value : this is another value
Annotation description : this is description
this 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