1. 程式人生 > >反射獲取一個方法中的參數名(不是類型)(轉)

反射獲取一個方法中的參數名(不是類型)(轉)

port 框架 boolean hand inpu 調用 length bool ret

https://www.cnblogs.com/guangshan/p/4660564.html

  一般來說,通過反射是很難獲得參數名的,只能取到參數類型,因為在編譯時,參數名有可能是會改變的,需要在編譯時加入參數才不會改變。

  使用註解是可以實現取類型名(或者叫註解名)的,但是要寫註解,並不方便。

  觀察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()表示是否編譯了參數名。

反射獲取一個方法中的參數名(不是類型)(轉)