Как SpringBoot анализирует параметры

Java

предисловие

Несколько дней назад, когда я писал интерфейс Rest, я увидел способ передачи значений, который я не писал раньше, поэтому мне пришла в голову идея его увидеть. До этого была статья, которая затрагивала эту тему, но эта статья была посвящена анализу потока обработки и не углублялась.

В этой статье рассматриваются несколько способов передачи параметров и показано, как все они анализируются и применяются к параметрам метода.

1. Процесс обработки HTTP-запроса

Будь то SpringBoot или SpringMVC, HTTP-запрос будетDispatcherServletКлассный прием, это важноServlet, потому что он наследуется отHttpServlet. Здесь Spring отвечает за разбор запроса, сопоставление сControllerМетод класса анализирует аргументы и выполняет метод, и, наконец, обрабатывает возвращаемое значение и отображает представление.

Наше внимание сегодня сосредоточено на разборе параметров, соответствующих приведенному выше.目标方法调用этот шаг. Поскольку речь идет об анализе параметров, должны быть разные анализаторы для разных типов параметров. Spring уже зарегистрировал для нас кучу всего этого.

у них общий интерфейсHandlerMethodArgumentResolver.supportsParameterИспользуется, чтобы определить, могут ли параметры метода быть проанализированы текущим синтаксическим анализатором, и если да, вызовитеresolveArgumentразобрать.

public interface HandlerMethodArgumentResolver {
    //判断方法参数是否可以被当前解析器解析
    boolean supportsParameter(MethodParameter var1);
    //解析参数
    @Nullable
    Object resolveArgument(MethodParameter var1, 
			@Nullable ModelAndViewContainer var2, 
			NativeWebRequest var3, 
			@Nullable WebDataBinderFactory var4)throws Exception;
}

2. Параметр Запроса

В методе Controller, если ваш параметр помеченRequestParamАннотация или простой тип данных.

@RequestMapping("/test1")
@ResponseBody
public String test1(String t1, @RequestParam(name = "t2",required = false) String t2,HttpServletRequest request){
	logger.info("参数:{},{}",t1,t2);
	return "Java";
}

Путь нашего запроса выглядит так:http://localhost:8080/test1?t1=Jack&t2=Java

Если в соответствии с предыдущим способом письма мы напрямую в соответствии с именем параметра илиRequestParamИмя аннотации берет значение из объекта Request. Например вот так:

String parameter = request.getParameter("t1");

В Spring соответствующий преобразователь параметров здесьRequestParamMethodArgumentResolver. Как и в нашей идее, после получения имени параметра получить значение непосредственно из запроса.

protected Object resolveName(String name, MethodParameter parameter, 
		NativeWebRequest request) throws Exception {
		
	HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
	//...省略部分代码...
	if (arg == null) {
		String[] paramValues = request.getParameterValues(name);
		if (paramValues != null) {
			arg = paramValues.length == 1 ? paramValues[0] : paramValues;
		}
	}
	return arg;
}

3. Тело запроса

Если нам нужно передать больше содержимого параметров во внешнем интерфейсе, это лучший способ передать параметры в теле через запрос POST. Конечно, более удобным форматом данных является JSON.

Столкнувшись с таким запросом, мы можем передать метод ControllerRequestBodyПримечания для его получения и автоматически преобразуются в соответствующий объект Java Bean.

@ResponseBody
@RequestMapping("/test2")
public String test2(@RequestBody SysUser user){
    logger.info("参数信息:{}",JSONObject.toJSONString(user));
    return "Hello";
}

Ввиду отсутствия Spring, давайте подумаем, как решить эту проблему?

Прежде всего, это все еще зависит от объекта Request. Для данных в Body мы можем передатьrequest.getReader()для получения, затем прочитайте строку и, наконец, преобразуйте ее в соответствующий объект Java с помощью служебного класса JSON.

Например вот так:

@RequestMapping("/test2")
@ResponseBody
public String test2(HttpServletRequest request) throws IOException {
    BufferedReader reader = request.getReader();
    StringBuilder builder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null){
    	builder.append(line);
    }
    logger.info("Body数据:{}",builder.toString());
    SysUser sysUser = JSONObject.parseObject(builder.toString(), SysUser.class);
    logger.info("转换后的Bean:{}",JSONObject.toJSONString(sysUser));
    return "Java";
}

Конечно, в фактическом сценарии вышеупомянутый Sysuser.Class должен динамически получить тип параметра.

Весной,RequestBodyПараметры аннотации будутRequestResponseBodyMethodProcessorкласс отвечает за синтаксический анализ.

Его разбор выполняется родительским классомAbstractMessageConverterMethodArgumentResolverответственный. Весь процесс разбит на три этапа.

1. Получить запрос вспомогательной информации

Перед началом необходимо получить некоторую вспомогательную информацию о запросе, такую ​​как формат данных HTTP-запроса, контекстную информацию о классе, тип параметра Class, тип метода HTTP-запроса и т. д.

protected <T> Object readWithMessageConverters(){
				   
	boolean noContentType = false;
	MediaType contentType;
	try {
		contentType = inputMessage.getHeaders().getContentType();
	} catch (InvalidMediaTypeException var16) {
		throw new HttpMediaTypeNotSupportedException(var16.getMessage());
	}
	if (contentType == null) {
		noContentType = true;
		contentType = MediaType.APPLICATION_OCTET_STREAM;
	}
	Class<?> contextClass = parameter.getContainingClass();
	Class<T> targetClass = targetType instanceof Class ? (Class)targetType : null;
	if (targetClass == null) {
		ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
		targetClass = resolvableType.resolve();
	}
	HttpMethod httpMethod = inputMessage instanceof HttpRequest ?
	        ((HttpRequest)inputMessage).getMethod() : null;	

	//.......
}

2. Определите конвертер сообщений

Для получения вышеуказанного эффекта является вспомогательная информация, она должна определить преобразователь сообщения. Конвертер сообщений имеет много, это их общие интерфейсыHttpMessageConverter. Здесь Spring зарегистрировал для нас много конвертеров, поэтому нам нужно просмотреть их, чтобы определить, какой из них использовать для преобразования сообщений.

Если он в формате данных JSON, он выберетMappingJackson2HttpMessageConverterобрабатывать. Его конструктор указывает именно на это.

public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
	super(objectMapper, new MediaType[]{
		MediaType.APPLICATION_JSON, 
		new MediaType("application", "*+json")});
}

3. Анализ

Теперь, когда определяет преобразователь сообщения, то остальные вещи очень просты. Получает тело по запросу, а затем вызовите анализа преобразователя просто отлично.

protected <T> Object readWithMessageConverters(){
    if (message.hasBody()) {
	  HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
	  body = genericConverter.read(targetType, contextClass, msgToUse);
	  body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
    }
}

Далее находится содержимое пакета Джексона, никаких подробностей. Хотя процесс его написания довольно затянут, на самом деле в основном нужно найти две вещи:

方法解析器RequestResponseBodyMethodProcessor

消息转换器MappingJackson2HttpMessageConverter

После того, как все найдено, можно вызвать метод для разбора.

Четыре, преобразование параметра запроса GET Bean

Есть и другой способ его записи, который получает Java Bean в методе Controller.

@RequestMapping("/test3")
@ResponseBody
public String test3(SysUser user){
    logger.info("参数:{}",JSONObject.toJSONString(user));
    return "Java";
}

Затем используйте метод GET для запроса:

http://localhost:8080/test3?id=1001&name=Jack&password=1234&address=北京市海淀区

Имя параметра за URL соответствует имени атрибута внутри объекта BEAL, или может быть автоматически преобразован. Итак, что это здесь делает?

Первое, что приходит к моему мнению, является механизм отражения Java. Получите имя параметра из объекта запроса, а затем установите значение в соответствии с одним на один на один метод на целевом классе.

Например вот так:

public String test3(SysUser user,HttpServletRequest request)throws Exception {
	//从Request中获取所有的参数key 和 value
	Map<String, String[]> parameterMap = request.getParameterMap();
	Iterator<Map.Entry<String, String[]>> iterator = parameterMap.entrySet().iterator();
	//获取目标类的对象
	Object target = user.getClass().newInstance();
	Field[] fields = target.getClass().getDeclaredFields();
	while (iterator.hasNext()){
		Map.Entry<String, String[]> next = iterator.next();
		String key = next.getKey();
		String value = next.getValue()[0];
		for (Field field:fields){
			String name = field.getName();
			if (key.equals(name)){
				field.setAccessible(true);
				field.set(target,value);
				break;
			}
		}
	}
	logger.info("userInfo:{}",JSONObject.toJSONString(target));
	return "Python";
}

Помимо отражения, в Java для этого есть механизм самоанализа. Мы можем получить объект дескриптора атрибута целевого класса, затем получить его объект Method и установить его с помощью вызова.

private void setProperty(Object target,String key,String value) {
    try {
	    PropertyDescriptor propDesc = new PropertyDescriptor(key, target.getClass());
	    Method method = propDesc.getWriteMethod();
	    method.invoke(target, value);
    } catch (Exception e) {
	    e.printStackTrace();
    }
}

Затем в приведенном выше цикле мы можем вызвать этот метод для достижения.

while (iterator.hasNext()){
	Map.Entry<String, String[]> next = iterator.next();
	String key = next.getKey();
	String value = next.getValue()[0];
	setProperty(userInfo,key,value);
}

Зачем говорить о механизмах самоанализа? Потому что, когда Spring занимается этим вопросом, он в конечном итоге решает его.

Проще говоря, это черезBeanWrapperImplиметь дело с. оBeanWrapperImplЕсть очень простой способ его использования:

SysUser user = new SysUser();
BeanWrapper wrapper = new BeanWrapperImpl(user.getClass());

wrapper.setPropertyValue("id","20001");
wrapper.setPropertyValue("name","Jack");

Object instance = wrapper.getWrappedInstance();
System.out.println(instance);

wrapper.setPropertyValueв конце концов позвонитBeanWrapperImpl#BeanPropertyHandler.setValue()метод.

этоsetValueметод и наш вышеsetPropertyМетод примерно такой же.

private class BeanPropertyHandler extends PropertyHandler {
    //属性描述符
    private final PropertyDescriptor pd;
    public void setValue(@Nullable Object value) throws Exception {
    	//获取set方法
    	Method writeMethod = this.pd.getWriteMethod();
    	ReflectionUtils.makeAccessible(writeMethod);
    	//设置
    	writeMethod.invoke(BeanWrapperImpl.this.getWrappedInstance(), value);
    }
}

С помощью описанного выше метода выполняется автоматическое преобразование параметров запроса GET в объекты Java Bean.

Оглядываясь назад, давайте снова посмотрим на Весну. Хотя то, что мы написали выше, очень просто, есть много вещей, которые следует учитывать при его использовании. Парсер в Spring, который обрабатывает этот тип параметра,ServletModelAttributeMethodProcessor.

Его процесс синтаксического анализа находится в его родительском классеModelAttributeMethodProcessor.resolveArgument()метод. Весь процесс также можно разделить на три этапа.

1. Получить конструктор целевого класса

В соответствии с типом параметра сначала создайте конструктор целевого класса для последующего использования при привязке данных.

2. Создайте связыватель данных WebDataBinder

WebDataBinderунаследовано отDataBinder. иDataBinderКороче говоря, основная функция заключается в использованииBeanWrapperУстановите значение для свойства объекта.

3. Привяжите данные к целевому классу и верните

Снова здесьWebDataBinderпреобразовать вServletRequestDataBinderObject, затем вызовите его метод BIND.

Следующим важным шагом является преобразование параметров в запросе вMutablePropertyValues pvsобъект.

Затем следующий шаг — зациклить pvs, вызвавsetPropertyValueУстановите свойства. Конечно, последний звонок на самом делеBeanWrapperImpl#BeanPropertyHandler.setValue().

Вот кусок кода для лучшего понимания процесса, эффект тот же:

//模拟Request参数
Map<String,Object> map = new HashMap();
map.put("id","1001");
map.put("name","Jack");
map.put("password","123456");
map.put("address","北京市海淀区");

//将request对象转换为MutablePropertyValues对象
MutablePropertyValues propertyValues = new MutablePropertyValues(map);
SysUser sysUser = new SysUser();
//创建数据绑定器
ServletRequestDataBinder binder = new ServletRequestDataBinder(sysUser);
//bind数据
binder.bind(propertyValues);
System.out.println(JSONObject.toJSONString(sysUser));

5. Пользовательский параметр Parser

Мы сказали, что все парсеры сообщений реализованы.HandlerMethodArgumentResolverинтерфейс. Мы также можем определить анализатор параметров и позволить ему реализовать этот интерфейс.

Во-первых, мы можем определитьRequestXunerаннотация.

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestXuner {
    String name() default "";
    boolean required() default false;
    String defaultValue() default "default";
}

Тогда это осознаетсяHandlerMethodArgumentResolverКласс анализатора интерфейса.

public class XunerArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestXuner.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter,
                                  ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest,
                                  WebDataBinderFactory webDataBinderFactory){
	
		//获取参数上的注解
        RequestXuner annotation = methodParameter.getParameterAnnotation(RequestXuner.class);
        String name = annotation.name();
		//从Request中获取参数值
        String parameter = nativeWebRequest.getParameter(name);
        return "HaHa,"+parameter;
    }
}

Не забывайте, что вам нужно настроить его.

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new XunerArgumentResolver());
    }
}

После операции в контроллере мы можем использовать это так:

@ResponseBody
@RequestMapping("/test4")
public String test4(@RequestXuner(name="xuner") String xuner){
    logger.info("参数:{}",xuner);
    return "Test4";
}

6. Резюме

Содержание этой статьи показывает процесс анализа параметров некоторыми парсерами в Spring с помощью соответствующего примера кода. Ведь как бы не менялись параметры, как бы ни были сложны типы параметров.

Все они отправляются через HTTP-запросы, затем вы можете пройтиHttpServletRequestчтобы получить все. Что делает Spring, так это использует аннотации, чтобы максимально адаптироваться к большинству сценариев приложений.