一般来说,通过反射是很难获得参数名的,只能取到参数类型,因为在编译时,参数名有可能是会改变的,需要在编译时加入参数才不会改变。
使用注解是可以实现取类型名(或者叫注解名)的,但是要写注解,并不方便。
观察Spring mvc框架中的数据绑定,发现是可以直接把http请求中对应参数绑定到对应的参数名上的,他是怎么实现的呢?
先参考一下自动绑定的原理:Spring源码研究:数据绑定
在getMethodArgumentValues方法中,MethodParameter[] parameters = getMethodParameters();这一句取到方法的所有参数,MethodParameter类型中有方法名的属性,这个是什么类呢?
是spring核心中的一个类,org.springframework.core.MethodParameter,并不是通过反射实现的。
方法getMethodParameters()是在HandlerMethod的类中
public MethodParameter[] getMethodParameters() {
return this.parameters;
}
this.parameters则是在构造方法中初始化的:
public HandlerMethod(Object bean, Method method) {
Assert.notNull(bean, "Bean is required");
Assert.notNull(method, "Method is required");
this.bean = bean;
this.beanFactory = null;
this.method = method;
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
this.parameters = initMethodParameters();
}
initMethodParameters()生成了参数列表。
private MethodParameter[] initMethodParameters() {
int count = this.bridgedMethod.getParameterTypes().length;
MethodParameter[] result = new MethodParameter[count];
for (int i = 0; i < count; i++) {
result[i] = new HandlerMethodParameter(i);
}
return result;
}
HandlerMethodParameter(i)是HandlerMethod的内部类,继承自MethodParameter
构造方法调用:
public HandlerMethodParameter(int index) {
super(HandlerMethod.this.bridgedMethod, index);
}
再调用MethodParameter类的构造方法:
public MethodParameter(Method method, int parameterIndex, int nestingLevel) {
Assert.notNull(method, "Method must not be null");
this.method = method;
this.parameterIndex = parameterIndex;
this.nestingLevel = nestingLevel;
this.constructor = null;
}
MethodParameter类中有private String parameterName;储存的就是参数名,但是构造方法中并没有设置他的值,真正设置值是在:
public String getParameterName() {
if (this.parameterNameDiscoverer != null) {
String[] parameterNames = (this.method != null ?
this.parameterNameDiscoverer.getParameterNames(this.method) :
this.parameterNameDiscoverer.getParameterNames(this.constructor));
if (parameterNames != null) {
this.parameterName = parameterNames[this.parameterIndex];
}
this.parameterNameDiscoverer = null;
}
return this.parameterName;
}
而parameterNameDiscoverer就是用来查找名称的,他在哪里设置的值呢?
public void initParameterNameDiscovery(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
这是个public方法,哪里调用了这个方法呢?有六七个地方吧,但是主要明显的是这里:
private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
args[i] = this.argumentResolvers.resolveArgument(
parameter, mavContainer, request, this.dataBinderFactory);
continue;
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
}
throw ex;
}
}
if (args[i] == null) {
String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
throw new IllegalStateException(msg);
}
}
return args;
}
又回到了初始方法,这里面对ParameterNameDiscovery初始化,用来查找参数名:
methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer);
this.parameterNameDiscoverer又是什么呢?
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
通过DefaultParameterNameDiscoverer类的实例来查找参数名。
/**
* Default implementation of the {@link ParameterNameDiscoverer} strategy interface,
* using the Java 8 standard reflection mechanism (if available), and falling back
* to the ASM-based {@link LocalVariableTableParameterNameDiscoverer} for checking
* debug information in the class file.
*
* <p>Further discoverers may be added through {@link #addDiscoverer(ParameterNameDiscoverer)}.
*
* @author Juergen Hoeller
* @since 4.0
* @see StandardReflectionParameterNameDiscoverer
* @see LocalVariableTableParameterNameDiscoverer
*/
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
private static final boolean standardReflectionAvailable =
(JdkVersion.getMajorJavaVersion() >= JdkVersion.JAVA_18);
public DefaultParameterNameDiscoverer() {
if (standardReflectionAvailable) {
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
}
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
}
这个是类声明,因为java1.8是支持从反射获取参数名的(具体参考网络)
低于1.8时使用new LocalVariableTableParameterNameDiscoverer()来解析参数名。
其中有方法:
public String[] getParameterNames(Method method) {
Method originalMethod = BridgeMethodResolver.findBridgedMethod(method);
Class<?> declaringClass = originalMethod.getDeclaringClass();
Map<Member, String[]> map = this.parameterNamesCache.get(declaringClass);
if (map == null) {
map = inspectClass(declaringClass);
this.parameterNamesCache.put(declaringClass, map);
}
if (map != NO_DEBUG_INFO_MAP) {
return map.get(originalMethod);
}
return null;
}
通过map = inspectClass(declaringClass);获取名称map。
private Map<Member, String[]> inspectClass(Class<?> clazz) {
InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz));
if (is == null) {
// We couldn\'t load the class file, which is not fatal as it
// simply means this method of discovering parameter names won\'t work.
if (logger.isDebugEnabled()) {
logger.debug("Cannot find \'.class\' file for class [" + clazz
+ "] - unable to determine constructors/methods parameter names");
}
return NO_DEBUG_INFO_MAP;
}
try {
ClassReader classReader = new ClassReader(is);
Map<Member, String[]> map = new ConcurrentHashMap<Member, String[]>(32);
classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0);
return map;
}
catch (IOException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Exception thrown while reading \'.class\' file for class [" + clazz +
"] - unable to determine constructors/methods parameter names", ex);
}
}
catch (IllegalArgumentException ex) {
if (logger.isDebugEnabled()) {
logger.debug("ASM ClassReader failed to parse class file [" + clazz +
"], probably due to a new Java class file version that isn\'t supported yet " +
"- unable to determine constructors/methods parameter names", ex);
}
}
finally {
try {
is.close();
}
catch (IOException ex) {
// ignore
}
}
return NO_DEBUG_INFO_MAP;
}
这是方法。。。由此可见,spring是直接读取class文件来读取参数名的。。。。。。。。。。。。真累
反射的method类型为public java.lang.String com.guangshan.data.DataPoolController.addData(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,java.lang.String,org.springframework.ui.Model)
所以需要通过类型查找参数名。
调试过程:反向调用过程:
1、
classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0);
classReader位于org.springframework.asm包中,是spring用于反编译的包,读取class信息,class信息中是包含参数名的(可以用文本编辑器打开一个class文件查看,虽然有乱码,但是方法的参数名还在)
通过accept填充map对象,map的键为成员名(方法名或者参数名),值为参数列表(字符串数组)。
2、
生成map之后,添加至参数名缓存,parameterNamesCache是以所在类的class为键,第一步的map为值的map。
3、
通过第一步的map获取方法中的参数名数组。
4、
通过调用本类parameterNameDiscoverer,再获取参数名的列表。
5、
6、
7、
最终回到数据绑定的方法
2016年6月6日11:45:59补充:
@Aspect的注解里面,参数有一个叫做JoinPoint的,这个JoinPoint里面也可以获取参数名:
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
MethodSignature有方法:public String[] getParameterNames()
实现在:MethodInvocationProceedingJoinPoint类的MethodSignatureImpl类中:
this.parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
parameterNameDiscoverer是:DefaultParameterNameDiscoverer:
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
private static final boolean standardReflectionAvailable =
(JdkVersion.getMajorJavaVersion() >= JdkVersion.JAVA_18);
public DefaultParameterNameDiscoverer() {
if (standardReflectionAvailable) {
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
}
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
}
判断版本,因为java8可以通过反射获取参数名,但是需要使用-parameters参数开启这个功能
可以看到有两个StandardReflectionParameterNameDiscoverer、LocalVariableTableParameterNameDiscoverer
一个是通过标准反射来获取,一个是通过解析字节码文件的本地变量表来获取的。
Parameter对象是新的反射对象,param.isNamePresent()表示是否编译了参数名。