SpringBoot08 请求方式参数获取注解参数验证前后台属性名不一致问题自定义参数验证注解BeanUtils的使用

Posted 寻渝记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot08 请求方式参数获取注解参数验证前后台属性名不一致问题自定义参数验证注解BeanUtils的使用相关的知识,希望对你有一定的参考价值。

 

1 请求方式

  在定义一个Rest接口时通常会利用GET、POST、PUT、DELETE来实现数据的增删改查;这几种方式有的需要传递参数,后台开发人员必须对接收到的参数进行参数验证来确保程序的健壮性

  1.1 GET

    一般用于查询数据,采用明文进行传输,一般用来获取一些无关用户信息的数据

  1.2 POST

    一般用于插入数据

  1.3 PUT

    一般用于数据更新

  1.4 DELETE

    一般用于数据删除

    技巧01:一般都是进行逻辑删除(即:仅仅改变记录的状态,而并非真正的删除数据)

2 参数获取注解

  2.1 @PathVariable

    路径参数,形如 url/{param} 时会用到,我们可以通过该注解来获取路径后面的参数来进行GET、DELETE、PUT操作

    

@GetMapping(value = "/{id}")
    public ResultViewModel findGirlById(
            @PathVariable("id") String id
    ) {
        // 01 参数去空格
        id = StringUtils.trim(id);

        // 02 判断参数是否全为数字
        Integer girlId = 0;
        if (JudgeUtil.allIsNumber(id)) {
            girlId = Integer.parseInt(id);
        } else {
            throw new GirlException(GirlEnum.PARAM_NOT_ALL_NUMBER);
        }

        // 03 调用服务层获取数据
        return ResultViewModelUtil.success(girlService.findGirlById(girlId));
    }
View Code

    2.1.1 为路径参数添加正则表达式

      如果前端请求的url中的路径参数不满足正则表达式时,请求就会失败;错误信息是找不到相应的请求方法

        

        代码解释:路径参数必须是数字类型,否则就不会匹配到这个请求路径,如果不是数字类型时返回给前端的信息如下:

          

  2.2 @RequestParam

    用来获取多个参数,常用语POST、PUT操作

    技巧01:这种参数时通过请求rul进行传递的,请求url中?后面的参数就是这种类型

    技巧02:默认就是利用这种方式进行传参

    技巧03:controller中方法参数可以利用一个对象来接收前端传过来的参数

  2.3 @RequestBody

    利用一个对象去获取前端传过来的数据

    技巧01:这种参数是通过请求体进行传递的

@PutMapping
    public ResultViewModel updateOneGirl(
            @Valid @RequestBody GirlFormModel girlFormModel,
            BindingResult result
            ) {
        // 01 参数验证
        if (result.hasErrors()) {
            // 0101 存储错误信息的字符串变量
            StringBuffer msgBuffer = new StringBuffer();
            // 0102 错误字段集合
            List<FieldError> fieldErrors = result.getFieldErrors();
            for (FieldError fieldError : fieldErrors) {
                // 0103 获取错误信息
                msgBuffer.append(fieldError.getField() + ":" + fieldError.getDefaultMessage());
            }
            // 0104 抛出错误信息
            throw new GirlException(GirlEnum.PARAM_ERROR.getCode(), msgBuffer.toString());
        }

        // 02 将表单对象转化成数据对象
        GirlModel girlModel = new GirlModel();
        BeanUtils.copyProperties(girlFormModel, girlModel);

        // 03 调用服务层进行更新操作
        Boolean updateResult = girlService.updateGirl(girlModel);
        if (updateResult) {
            return ResultViewModelUtil.success(GirlEnum.UPDATE_SUCCESS.getMessage());
        } else {
            return ResultViewModelUtil.error(GirlEnum.UPDATE_ERROR.getCode(), GirlEnum.UPDATE_ERROR.getMessage());
        }
    }
View Code

 

 3 参数验证注解

  后台单独对接收到的参数进行验证时比较麻烦,springboot项目的web组件集成了hibernate-validator,开发者可以直接使用hibernate-validator提供的注解对数据进行校验,当有一些复杂的参数校验时我们也可以自定义校验注解

  技巧01:接收到的参数默认都是字符串类型的

  技巧02:如果是SpringBoot项目必须导入JPA先关的jar包,因为JPA是基于Hibernate的,所以JPA相关的jar包里面会有相关的验证注解;其实不用导入JPA的jar包也可以使用,因为 spring-boot-starter-web 中已经包含了 hibernate-validator 相关的校验注解。

  坑01:有的注解只能用在String类型的属性上

  3.1 常用的验证注解有

    

    技巧01:@JsonProperty可以实现前端的属性名和后台实体类的属性名不一致问题,例如

      

      代码解释:前端传过来的属性名为nick,后台接收的属性名为nickname

      技巧01:前端传过来的参数和后台不一致的情况只对@RequestBody这种参数有效

      技巧02:后台传给前台时使用的属性名是@JsonProperty中的参数而不是实体中定义的属性名

  3.2 使用方法

    直接在表单实体类的属性上面添加相应的注解就可以啦

package cn.xiangxu.springboottest.model.dataFormModel;


import cn.xiangxu.springboottest.commons.validators.GirlFormIdValidator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;

import javax.validation.constraints.Min;

@Data
public class GirlFormModel {

//    @NotBlank(message = "目标girl的ID不能为空")
//    @NotEmpty(message = "目标girl的ID不能为空哟")
//    @GirlFormIdValidator
    private Integer girlId;



    @Min(value = 123)
    private Integer age;

    private String name;

    private String password;

    @NotEmpty(message = "昵称不能为空")
    @NotBlank(message = "昵称不能为空哟")
    @JsonProperty("nick") //  当前端属性为nick后台接收对象的属性为nickName时可以用@JsonProperty来保持一致
    private String nickname;

    @NotEmpty(message = "地址不能为空")
    @NotBlank(message = "地址不能为空哟")
    private String address;
}
View Code

  3.3 如何使参数校验生效

    3.3.1 在控制层方法的形参前面添加@Valid注解

      技巧01:如果在controller层控制方法中的参数标注了@Valid注解后,前端传过来的参数一旦发现不合法就会抛出请求错误(即:400错误)

      

    3.3.2 利用BindingResult对象获取参数错误字段和参数错误信息

      技巧01:如果只是添加@Valid注解,如果参数错误就会直接抛出异常

      技巧02:后台开发时通常都需要对前端传过来的参数进行一次参数验证,如果参数不正确我们需要抛出一些用户可以识别的错误信息而不是抛出原生的错误;所以我们需要在方法中添加一个BindingResult参数

      技巧03:给控制方法添加了BindingResult参数后,即使前端传过来的参数不合法也会继续执行方法体;我们可以利用BindingResult对象去判断时哪一个参数不合法,从而抛出一些自定义的异常信息

      》获取所有错误字段和错误信息的代码如下

       

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.validation;

import java.beans.PropertyEditor;
import java.util.Map;
import org.springframework.beans.PropertyEditorRegistry;

public interface BindingResult extends Errors {
    String MODEL_KEY_PREFIX = BindingResult.class.getName() + ".";

    Object getTarget();

    Map<String, Object> getModel();

    Object getRawFieldValue(String var1);

    PropertyEditor findEditor(String var1, Class<?> var2);

    PropertyEditorRegistry getPropertyEditorRegistry();

    void addError(ObjectError var1);

    String[] resolveMessageCodes(String var1);

    String[] resolveMessageCodes(String var1, String var2);

    void recordSuppressedField(String var1);

    String[] getSuppressedFields();
}
BindingResult源码

       》获取单个错误字段和错误信息的代码如下

        

    @PostMapping(value = "/user")
    public String create(
            @Valid @RequestBody User user,
            BindingResult errors
    ) {
        if (errors.hasErrors()) {
            errors.getAllErrors().stream().forEach(
                    error -> {
                        FieldError fieldError = (FieldError)error;
                        log.info("错误字段:{} -> 错误信息:{}",
                                fieldError.getField(),
                                fieldError.getDefaultMessage()
                        );
                    }
            );
        }

        log.info("获取到的参数信息为:" + user);
        String info = "新增用户成功";
        return info;
    }
View Code

      技巧04:利用BindingResult对象的hasErrors方法判断是否有参数错误

    ·  技巧05:利用BindingResult对象的getFieldErrors方法获取所有有参数错误的属性

       技巧06:利用错误属性对象的getDefaultMessage去获取错误提示信息

 

4 自定义参数验证注解

  4.1 定义一个注解接口

    技巧01:每个自定义的校验注解必须要有这四个成员

    

package cn.xiangxu.springboottest.commons.validators;

import cn.xiangxu.springboottest.commons.validators.validatorClass.GirlFormValidatorClass;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Constraint(validatedBy = GirlFormValidatorClass.class)
public @interface GirlFormIdValidator {
    String values();
    String message() default "girl的ID必须为纯数字";

    Class<?>[] groups() default {};
    Class<? extends Payload>[]  payload() default {};

}
View Code

  4.2 定义一个注解接口实现类

    技巧01:必须实现ConstraintValidator接口

    技巧02:实现了ConstraintValidator接口后即使不进行Bean配置,spring也会将这个类进行Bean管理

    技巧03:可以在实现了ConstraintValidator接口的类中依赖注入其它Bean

    技巧04:实现了ConstraintValidator接口后必须重写 initialize 和 isValid 这两个方法;initialize方法主要来进行初始化,通常用来获取自定义注解的属性值;isValid 方法主要进行校验逻辑,返回true表示校验通过,返回false表示校验失败,通常根据注解属性值和实体类属性值进行校验判断

    

package cn.xiangxu.springboottest.commons.validators.validatorClass;

import cn.xiangxu.springboottest.commons.validators.GirlFormIdValidator;
import cn.xiangxu.springboottest.utils.JudgeUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class GirlFormValidatorClass implements ConstraintValidator<GirlFormIdValidator, Object> {

    private String values;

    @Override
    public void initialize(GirlFormIdValidator girlFormValidator) {
        this.values = girlFormValidator.values();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        if (JudgeUtil.allIsNumber((String)value)) {
            return true;
        }
        return false;
    }
}
View Code

  4.3 在实体类中使用自定义的参数校验注解

    

 

5 BeanUtils的使用

  利用BeanUtils提供的copyProperties可以实现实体类数据的复制

  坑01:只用属性名和属性类型一致的部分才可以进行复制操作,例如

    BeanUtils.copyProperties(girlFormModel, girlModel);

    代码解释:将girlFormModel对象复制给girlModel对象

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.beans;

import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

public abstract class BeanUtils {
    private static final Log logger = LogFactory.getLog(BeanUtils.class);
    private static final Set<Class<?>> unknownEditorTypes = Collections.newSetFromMap(new ConcurrentReferenceHashMap(64));

    public BeanUtils() {
    }

    public static <T> T instantiate(Class<T> clazz) throws BeanInstantiationException {
        Assert.notNull(clazz, "Class must not be null");
        if (clazz.isInterface()) {
            throw new BeanInstantiationException(clazz, "Specified class is an interface");
        } else {
            try {
                return clazz.newInstance();
            } catch (InstantiationException var2) {
                throw new BeanInstantiationException(clazz, "Is it an abstract class?", var2);
            } catch (IllegalAccessException var3) {
                throw new BeanInstantiationException(clazz, "Is the constructor accessible?", var3);
            }
        }
    }

    public static <T> T instantiateClass(Class<T> clazz) throws BeanInstantiationException {
        Assert.notNull(clazz, "Class must not be null");
        if (clazz.isInterface()) {
            throw new BeanInstantiationException(clazz, "Specified class is an interface");
        } else {
            try {
                return instantiateClass(clazz.getDeclaredConstructor());
            } catch (NoSuchMethodException var2) {
                throw new BeanInstantiationException(clazz, "No default constructor found", var2);
            }
        }
    }

    public static <T> T instantiateClass(Class<?> clazz, Class<T> assignableTo) throws BeanInstantiationException {
        Assert.isAssignable(assignableTo, clazz);
        return instantiateClass(clazz);
    }

    public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
        Assert.notNull(ctor, "Constructor must not be null");

        try {
            ReflectionUtils.makeAccessible(ctor);
            return ctor.newInstance(args);
        } catch (InstantiationException var3) {
            throw new BeanInstantiationException(ctor, "Is it an abstract class?", var3);
        } catch (IllegalAccessException var4) {
            throw new BeanInstantiationException(ctor, "Is the constructor accessible?", var4);
        } catch (IllegalArgumentException var5) {
            throw new BeanInstantiationException(ctor, "Illegal arguments for constructor", var5);
        } catch (InvocationTargetException var6) {
            throw new BeanInstantiationException(ctor, "Constructor threw exception", var6.getTargetException());
        }
    }

    public static Method findMethod(Class<?> clazz, String methodName, Class... paramTypes) {
        try {
            return clazz.getMethod(methodName, paramTypes);
        } catch (NoSuchMethodException var4) {
            return findDeclaredMethod(clazz, methodName, paramTypes);
        }
    }

    public static Method findDeclaredMethod(Class<?> clazz, String methodName, Class... paramTypes) {
        try {
            return clazz.getDeclaredMethod(methodName, paramTypes);
        } catch (NoSuchMethodException var4) {
            return clazz.getSuperclass() != null ? findDeclaredMethod(clazz.getSuperclass(), methodName, paramTypes) : null;
        }
    }

    public static Method findMethodWithMinimalParameters(Class<?> clazz, String methodName) throws IllegalArgumentException {
        Method targetMethod = findMethodWithMinimalParameters(clazz.getMethods(), methodName);
        if (targetMethod == null) {
            targetMethod = findDeclaredMethodWithMinimalParameters(clazz, methodName);
        }

        return targetMethod;
    }

    public static Method findDeclaredMethodWithMinimalParameters(Class<?> clazz, String methodName) throws IllegalArgumentException {
        Method targetMethod = findMethodWithMinimalParameters(clazz.getDeclaredMethods(), methodName);
        if (targetMethod == null && clazz.getSuperclass() != null) {
            targetMethod = findDeclaredMethodWithMinimalParameters(clazz.getSuperclass(), methodName);
        }

        return targetMethod;
    }

    public static Method findMethodWithMinimalParameters(Method[] methods, String methodName) throws IllegalArgumentException {
        Method targetMethod = null;
        int numMethodsFoundWithCurrentMinimumArgs = 0;
        Method[] var4 = methods;
        int var5 = methods.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Method method = var4[var6];
            if (method.getName().equals(methodName)) {
                int numParams = method.getParameterTypes().length;
                if (targetMethod != null && numParams >= targetMethod.getParameterTypes().length) {
                    if (!method.isBridge() && targetMethod.getParameterTypes().length == numParams) {
                        if (targetMethod.isBridge()) {
                            targetMethod = method;
                        } else {
                            ++numMethodsFoundWithCurrentMinimumArgs;
                        }
                    }
                } else {
                    targetMethod = method;
                    numMethodsFoundWithCurrentMinimumArgs = 1;
                }
            }
        }

        if (numMethodsFoundWithCurrentMinimumArgs > 1) {
            throw new IllegalArgumentException("Cannot resolve method \'" + methodName + "\' to a unique method. Attempted to resolve to overloaded method with the least number of parameters but there were " + numMethodsFoundWithCurrentMinimumArgs + " candidates.");
        } else {
            return targetMethod;
        }
    }

    public static Method resolveSignature(String signature, Class<?> clazz) {
        Assert.hasText(signature, "\'signature\' must not be empty");
        Assert.notNull(clazz, "Class must not be null");
        int firstParen = signature.indexOf("(");
        int lastParen = signature.indexOf(")");
        if (firstParen > -1 && lastParen == -1) {
            throw new IllegalArgumentException("Invalid method signature \'" + signature + "\': expected closing \')\' for args list");
        } else if (lastParen > -1 && firstParen == -1) {
            throw new IllegalArgumentException("Invalid method signature \'" + signature + "\': expected opening \'(\' for args list");
        } else if (firstParen == -1 && lastParen == -1) {
            return findMethodWithMinimalParameters(clazz, signature);
        } else {
            String methodName = signature.substring(0, firstParen);
            String[] parameterTypeNames = StringUtils.commaDelimitedListToStringArray(signature.substring(firstParen + 1, lastParen));
            Class<?>[] parameterTypes = new Class[parameterTypeNames.length];

            for(int i = 0; i < parameterTypeNames.length; ++i) {
                String parameterTypeName = parameterTypeNames[i].trim();

                try {
                    parameterTypes[i] = ClassUtils.forName(parameterTypeName, clazz.getClassLoader());
                } catch (Throwable var10) {
                    throw new IllegalArgumentException("Invalid method signature: unable to resolve type [" + parameterTypeName + "] for argument " + i + ". Root cause: " + var10);
                }
            }

            return findMethod(clazz, methodName, parameterTypes);
        }
    }

    public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
        CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);
        return cr.getPropertyDescriptors();
    }

    public static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String propertyName) throws BeansException {
        CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);
        return cr.getPropertyDescriptor(propertyName);
    }

    public static PropertyDescriptor findPropertyForMethod(Method method) throws BeansException {
        return findPropertyForMethod(method, method.getDeclaringClass());
    }

    public static PropertyDescriptor findPropertyForMethod(Method method, Class<?> clazz) throws BeansException {
        Assert.notNull(method, "Method must not be null");
        PropertyDescriptor[] pds = getPropertyDescriptors(clazz);
        PropertyDescriptor[] var3 = pds;
        int var4 = pds.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            PropertyDescriptor pd = var3[var5];
            if (method.equals(pd.getReadMethod()) || method.equals(pd.getWriteMethod())) {
                return pd;
            }
        }

        return null;
    }

    public static PropertyEditor findEditorByConvention(Class<?> targetType) {
        if (targetType != null && !targetType.isArray() && !unknownEditorTypes.contains(targetType)) {
            ClassLoader cl = targetType.getClassLoader();
            if (cl == null) {
                try {
                    cl = ClassLoader.getSystemClassLoader();
                    if (cl == null) {
                        return null;
                    }
                } catch (Throwable var5) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Could not access system ClassLoader: " + var5);
                    }

                    return null;
                }
            }

            String editorName = targetType.getName() + "Editor";

            try {
                Class<?> editorClass = cl.loadClass(editorName);
                if (!PropertyEditor.class.isAssignableFrom(editorClass)) {
                    if (logger.isWarnEnabled()) {
                        logger.warn("Editor class [" + editorName + "] does not implement [java.beans.PropertyEditor] interface");
                    }

                    unknownEditorTypes.add(targetType);
                    return null;
                } else {
                    return (PropertyEditor)instantiateClass(editorClass);
                }
            } catch (ClassNotFoundException var4) {
                if (logger.isDebugEnabled()) {
                    logger.debug("No property editor [" + editorName + "] found for type " + targetType.getName() + " according to \'Editor\' suffix convention");
                }

                unknownEditorTypes.add(targetType);
                return null;
            }
        } else {
            return null;
        }
    }

    public static Class<?> findPropertyType(String propertyName, Class... beanClasses) {
        if (beanClasses != null) {
            Class[] var2 = beanClasses;
            int var3 = beanClasses.length;

            for(int var4 = 0; var4 < var3; ++var4) {
                Class<?> beanClass = var2[var4];
                PropertyDescriptor pd = getPropertyDescriptor(beanClass, propertyName);
                if (pd != null) {
                    return pd.getPropertyType();
                }
            }
        }

        return Object.class;
    }

    public static MethodParameter getWriteMethodParameter(PropertyDescriptor pd) {
        return pd instanceof GenericTypeAwarePropertyDescriptor ? new MethodParameter(((GenericTypeAwarePropertyDescriptor)pd).getWriteMethodParameter()) : new MethodParameter(pd.getWriteMethod(), 0);
    }

    public static boolean isSimpleProperty(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        return isSimpleValueType(clazz) || clazz.isArray() && isSimpleValueType(clazz.getComponentType());
    }

    public static boolean isSimpleValueType(Class<?> clazz) {
        return ClassUtils.isPrimitiveOrWrapper(clazz) || clazz.isEnum() || CharSequence.class.isAssignableFrom(clazz) || Number.class.isAssignableFrom(clazz) || Date.class.isAssignableFrom(clazz) || URI.class == clazz || URL.class == clazz || Locale.class == clazz || Class.class == clazz;
    }

    public static void copyProperties(Object source, Object target) throws BeansException {
        copyProperties(source, target, (Class)null, (String[])null);
    }

    public static void copyProperties(Object source, Object target, Class<?> editable) throws BeansException {
        copyProperties(source, target, editable, (String[])null);
    }

    public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
        copyProperties(source, target, (Class)null, ignoreProperties);
    }

    private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties) throws BeansException {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");
        Class<?> actualEditable = target.getClass();
        if (editable != null) {
            if (!editable.isInstance(target)) {
                throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
            }

            actualEditable = editable;
        }

        PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
        List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
        PropertyDescriptor[] var7 = targetPds;
        int var8 = targetPds.length;

        for(int var9 = 0; var9 < var8; ++var9) {
            PropertyDescriptor targetPd = var7[var9];
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                if (sourcePd != null) {
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try以上是关于SpringBoot08 请求方式参数获取注解参数验证前后台属性名不一致问题自定义参数验证注解BeanUtils的使用的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot Controller接收参数的几种常用注解方式

SpringBoot 学习笔记心得请求参数处理

springboot通过HttpServletRequestWrapper获取所有请求参数

springboot controller获取请求参数的注解

【SpringBoot】http请求注解之Post请求

SpringBoot2各类型参数解析原理(源码分析)