springboot系列--web相关知识探索五
一、前言
web相关知识探索四中研究了请求中所带的参数是如何映射到接口参数中的,也即请求参数如何与接口参数绑定。主要有四种、分别是注解方式、Servlet API方式、复杂参数、以及自定义对象参数。web相关知识探索四中主要研究了复杂参数底层绑定原理。本次主要是研究自定义对象参数数据绑定底层原理。
二、原理
一、测试用例
@Data
public class Person {private String userName;private Integer age;private Date birth;private Pet pet;@Datapublic static class Pet {private String name;private String age;}}@PostMapping("/test")public Person testEntity(Person person){System.out.println(JSONUtil.toJsonStr(person));return person;}
二、postman请求方式
三、原理
一、寻找参数解析器
请求进来以后,直接到匹配合适的参数解析器这一步,由于我们的参数只有一个,也就是Person类型的自定义对象参数。可以找到是这个参数处理器处理自定义JavaBean参数。
ServletModelAttributeMethodProcessor
// org.springframework.web.method.annotation包下的ModelAttributeMethodProcessor类里面的方法
// 判断当前参数,这个解析器支不支持解析public boolean supportsParameter(MethodParameter parameter) {// 判断条件为,是否使用了ModelAttribute这个注解,或者annotationNotRequired这个属性是否为true也就是这个注解不是必须得,且这个参数是不是简单类型return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType());}// 简单类型判断标准public static boolean isSimpleProperty(Class<?> type) {Assert.notNull(type, "'type' must not be null");return isSimpleValueType(type) || type.isArray() && isSimpleValueType(type.getComponentType());}// 数字,字符串,void、枚举等等public static boolean isSimpleValueType(Class<?> type) {return Void.class != type && Void.TYPE != type && (ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type);}
二、参数解析
一、请求参数到目标参数的数据类型转换,这部分只是获取转换器,到了准备转换数据类型步骤
// 开始进行参数解析@Nullablepublic final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");String name = ModelFactory.getNameForParameter(parameter);ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);if (ann != null) {mavContainer.setBinding(name, ann.binding());}Object attribute = null;BindingResult bindingResult = null;if (mavContainer.containsAttribute(name)) {attribute = mavContainer.getModel().get(name);} else {// 这里以上都不用管,以上是使用ModelAttribute注解处理的逻辑try {// 创建一个空的参数对象,也就是接口参数里面的自定义javaBean对象,但是里面的属性值是空的。之后就会让这个对象的属性值与请求进来所带的参数进行一一绑定。attribute = this.createAttribute(name, parameter, binderFactory, webRequest);} catch (BindException var10) {if (this.isBindExceptionRequired(parameter)) {throw var10;}if (parameter.getParameterType() == Optional.class) {attribute = Optional.empty();} else {attribute = var10.getTarget();}bindingResult = var10.getBindingResult();}}// 如果没有请求进来的参数进行绑定,就会进入下面进行绑定if (bindingResult == null) {// 创建绑定器,这里会把原生请求对象以及空属性person对象封装进去。同时还有各种类型的转换器,// 例如,所有请求都是通过http超文本协议传过来的,所以所有请求参数都是文本类型也就是String,比如年龄12,请求进来就会把"12"字符串类型转为Integer类型WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);if (binder.getTarget() != null) {if (!mavContainer.isBindingDisabled(name)) {// 这里就进行了参数绑定,也就是给Person绑定上了请求所携带的值。主要逻辑在里面this.bindRequestParameters(binder, webRequest);}this.validateIfApplicable(binder, parameter);if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {throw new BindException(binder.getBindingResult());}}if (!parameter.getParameterType().isInstance(attribute)) {attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);}bindingResult = binder.getBindingResult();}Map<String, Object> bindingResultModel = bindingResult.getModel();mavContainer.removeAttributes(bindingResultModel);mavContainer.addAllAttributes(bindingResultModel);return attribute;}// 这里会调用父类的方法创建一个Perrson对象protected final Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {String value = this.getRequestValueForAttribute(attributeName, request);if (value != null) {Object attribute = this.createAttributeFromRequestValue(value, attributeName, parameter, binderFactory, request);if (attribute != null) {return attribute;}}// 会进到这里return super.createAttribute(attributeName, parameter, binderFactory, request);}// 具体创建一个person对象逻辑protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {// 通过MethodParameter对象获取接口参数MethodParameter nestedParameter = parameter.nestedIfOptional();// 获取接口参数类型Class<?> clazz = nestedParameter.getNestedParameterType();// 获取参数类型对应的构造函数Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);// 这里就是通过底层反射之类的构造Perrson对象Object attribute = this.constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);if (parameter != nestedParameter) {attribute = Optional.of(attribute);}return attribute;}// 这里就进行了参数绑定,ServletModelAttributeMethodProcessor中的bindRequestParameters方法protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {// 获取请求ServletRequest servletRequest = (ServletRequest)request.getNativeRequest(ServletRequest.class);Assert.state(servletRequest != null, "No ServletRequest");ServletRequestDataBinder servletBinder = (ServletRequestDataBinder)binder;// 这里进行了数据绑定servletBinder.bind(servletRequest);}// 对应上面的bind数据绑定方法,ServletRequestDataBinder类里面的public void bind(ServletRequest request) {// 获取请求中参数的k-v数据对MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);MultipartRequest multipartRequest = (MultipartRequest)WebUtils.getNativeRequest(request, MultipartRequest.class);if (multipartRequest != null) {this.bindMultipart(multipartRequest.getMultiFileMap(), mpvs);} else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) {HttpServletRequest httpServletRequest = (HttpServletRequest)WebUtils.getNativeRequest(request, HttpServletRequest.class);if (httpServletRequest != null) {StandardServletPartUtils.bindParts(httpServletRequest, mpvs, this.isBindEmptyMultipartFiles());}}this.addBindValues(mpvs, request);// 这里是真正进行参数绑定的方法this.doBind(mpvs);}// WebDataBinder类里面的绑定方法protected void doBind(MutablePropertyValues mpvs) {this.checkFieldDefaults(mpvs);this.checkFieldMarkers(mpvs);this.adaptEmptyArrayIndices(mpvs);// 主要是调用父类的绑定方法super.doBind(mpvs);}// 父类DataBinder里面的绑定方法protected void doBind(MutablePropertyValues mpvs) {this.checkAllowedFields(mpvs);this.checkRequiredFields(mpvs);// 主要是这个方法this.applyPropertyValues(mpvs);}// 这个方法也是DataBinder里面的protected void applyPropertyValues(MutablePropertyValues mpvs) {try {// 设置属性值this.getPropertyAccessor().setPropertyValues(mpvs, this.isIgnoreUnknownFields(), this.isIgnoreInvalidFields());} catch (PropertyBatchUpdateException var7) {PropertyAccessException[] var3 = var7.getPropertyAccessExceptions();int var4 = var3.length;for(int var5 = 0; var5 < var4; ++var5) {PropertyAccessException pae = var3[var5];this.getBindingErrorProcessor().processPropertyAccessException(pae, this.getInternalBindingResult());}}}// 具体设置属性值的逻辑,AbstractPropertyAccessor这个类里面的方法public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException {List<PropertyAccessException> propertyAccessExceptions = null;// 获取所有需要设置进去的属性值,也就是请求形成的k-v对List<PropertyValue> propertyValues = pvs instanceof MutablePropertyValues ? ((MutablePropertyValues)pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues());if (ignoreUnknown) {this.suppressNotWritablePropertyException = true;}try {// 循环遍历数据Iterator var6 = propertyValues.iterator();while(var6.hasNext()) {PropertyValue pv = (PropertyValue)var6.next();try {// 设置属性值this.setPropertyValue(pv);} catch (NotWritablePropertyException var14) {if (!ignoreUnknown) {throw var14;}} catch (NullValueInNestedPathException var15) {if (!ignoreInvalid) {throw var15;}} catch (PropertyAccessException var16) {if (propertyAccessExceptions == null) {propertyAccessExceptions = new ArrayList();}propertyAccessExceptions.add(var16);}}} finally {if (ignoreUnknown) {this.suppressNotWritablePropertyException = false;}}if (propertyAccessExceptions != null) {PropertyAccessException[] paeArray = (PropertyAccessException[])propertyAccessExceptions.toArray(new PropertyAccessException[0]);throw new PropertyBatchUpdateException(paeArray);}}// 这段源码就是上面设置属性值的具体代码,AbstractNestablePropertyAccessor类里面的方法public void setPropertyValue(PropertyValue pv) throws BeansException {PropertyTokenHolder tokens = (PropertyTokenHolder)pv.resolvedTokens;if (tokens == null) {String propertyName = pv.getName();AbstractNestablePropertyAccessor nestedPa;try {nestedPa = this.getPropertyAccessorForPropertyPath(propertyName);} catch (NotReadablePropertyException var6) {throw new NotWritablePropertyException(this.getRootClass(), this.nestedPath + propertyName, "Nested property in path '" + propertyName + "' does not exist", var6);}tokens = this.getPropertyNameTokens(this.getFinalPath(nestedPa, propertyName));if (nestedPa == this) {pv.getOriginalPropertyValue().resolvedTokens = tokens;}// 这里是利用反射进行设置值的核心逻辑nestedPa.setPropertyValue(tokens, pv);} else {this.setPropertyValue(tokens, pv);}}// AbstractNestablePropertyAccessor类里面的方法protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {if (tokens.keys != null) {this.processKeyedProperty(tokens, pv);} else {this.processLocalProperty(tokens, pv);}}// 设置属性值的具体逻辑,AbstractNestablePropertyAccessor类里面的方法private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {PropertyHandler ph = this.getLocalPropertyHandler(tokens.actualName);if (ph != null && ph.isWritable()) {Object oldValue = null;PropertyChangeEvent propertyChangeEvent;try {// 拿到请求中具体的值,例如年龄 12等等Object originalValue = pv.getValue();Object valueToApply = originalValue;if (!Boolean.FALSE.equals(pv.conversionNecessary)) {if (pv.isConverted()) {valueToApply = pv.getConvertedValue();} else {if (this.isExtractOldValueForEditor() && ph.isReadable()) {try {oldValue = ph.getValue();} catch (Exception var8) {Exception ex = var8;if (var8 instanceof PrivilegedActionException) {ex = ((PrivilegedActionException)var8).getException();}if (logger.isDebugEnabled()) {logger.debug("Could not read previous value of property '" + this.nestedPath + tokens.canonicalName + "'", ex);}}}// 这里是最主要的,首先会进行请求参数的数据转换,例如将String类型的年龄转为Integer类型的年龄。。valueToApply = this.convertForProperty(tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());}pv.getOriginalPropertyValue().conversionNecessary = valueToApply != originalValue;}ph.setValue(valueToApply);} catch (TypeMismatchException var9) {throw var9;} catch (InvocationTargetException var10) {propertyChangeEvent = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());if (var10.getTargetException() instanceof ClassCastException) {throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), var10.getTargetException());} else {Throwable cause = var10.getTargetException();if (cause instanceof UndeclaredThrowableException) {cause = cause.getCause();}throw new MethodInvocationException(propertyChangeEvent, cause);}} catch (Exception var11) {propertyChangeEvent = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());throw new MethodInvocationException(propertyChangeEvent, var11);}} else if (pv.isOptional()) {if (logger.isDebugEnabled()) {logger.debug("Ignoring optional value for property '" + tokens.actualName + "' - property not found on bean class [" + this.getRootClass().getName() + "]");}} else if (!this.suppressNotWritablePropertyException) {throw this.createNotWritablePropertyException(tokens.canonicalName);}}// 数据转换器,AbstractNestablePropertyAccessor类里面的方法@Nullableprivate Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<?> requiredType, @Nullable TypeDescriptor td) throws TypeMismatchException {Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");PropertyChangeEvent pce;try {// 这里使用了代理,进入查看数据是如何转化的return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);} catch (IllegalStateException | ConverterNotFoundException var8) {pce = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + propertyName, oldValue, newValue);throw new ConversionNotSupportedException(pce, requiredType, var8);} catch (IllegalArgumentException | ConversionException var9) {pce = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + propertyName, oldValue, newValue);throw new TypeMismatchException(pce, requiredType, var9);}}// 数据转化具体逻辑,TypeConverterDelegate里面的方法@Nullablepublic <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);ConversionFailedException conversionAttemptEx = null;// 这里是获取124个转换器服务ConversionService conversionService = this.propertyEditorRegistry.getConversionService();if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);// 然后判断那个转换器能够进行转换if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {try {// 这里就开始进行数据转换了return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);} catch (ConversionFailedException var14) {conversionAttemptEx = var14;}}}Object convertedValue = newValue;if (editor != null || requiredType != null && !ClassUtils.isAssignableValue(requiredType, newValue)) {if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) && newValue instanceof String) {TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor();if (elementTypeDesc != null) {Class<?> elementType = elementTypeDesc.getType();if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {convertedValue = StringUtils.commaDelimitedListToStringArray((String)newValue);}}}if (editor == null) {editor = this.findDefaultEditor(requiredType);}convertedValue = this.doConvertValue(oldValue, convertedValue, requiredType, editor);}boolean standardConversion = false;if (requiredType != null) {if (convertedValue != null) {if (Object.class == requiredType) {return convertedValue;}if (requiredType.isArray()) {if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) {convertedValue = StringUtils.commaDelimitedListToStringArray((String)convertedValue);}return this.convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType());}if (convertedValue instanceof Collection) {convertedValue = this.convertToTypedCollection((Collection)convertedValue, propertyName, requiredType, typeDescriptor);standardConversion = true;} else if (convertedValue instanceof Map) {convertedValue = this.convertToTypedMap((Map)convertedValue, propertyName, requiredType, typeDescriptor);standardConversion = true;}if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) {convertedValue = Array.get(convertedValue, 0);standardConversion = true;}if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {return convertedValue.toString();}if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) {if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) {try {Constructor<T> strCtor = requiredType.getConstructor(String.class);return BeanUtils.instantiateClass(strCtor, new Object[]{convertedValue});} catch (NoSuchMethodException var12) {if (logger.isTraceEnabled()) {logger.trace("No String constructor found on type [" + requiredType.getName() + "]", var12);}} catch (Exception var13) {if (logger.isDebugEnabled()) {logger.debug("Construction via String failed for type [" + requiredType.getName() + "]", var13);}}}String trimmedValue = ((String)convertedValue).trim();if (requiredType.isEnum() && trimmedValue.isEmpty()) {return null;}convertedValue = this.attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue);standardConversion = true;} else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) {convertedValue = NumberUtils.convertNumberToTargetClass((Number)convertedValue, requiredType);standardConversion = true;}} else if (requiredType == Optional.class) {convertedValue = Optional.empty();}if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) {if (conversionAttemptEx != null) {throw conversionAttemptEx;}if (conversionService != null && typeDescriptor != null) {TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);}}StringBuilder msg = new StringBuilder();msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue));msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append("'");if (propertyName != null) {msg.append(" for property '").append(propertyName).append("'");}if (editor != null) {msg.append(": PropertyEditor [").append(editor.getClass().getName()).append("] returned inappropriate value of type '").append(ClassUtils.getDescriptiveType(convertedValue)).append("'");throw new IllegalArgumentException(msg.toString());}msg.append(": no matching editors or conversion strategy found");throw new IllegalStateException(msg.toString());}}if (conversionAttemptEx != null) {if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {throw conversionAttemptEx;}logger.debug("Original ConversionService attempt failed - ignored since PropertyEditor based conversion eventually succeeded", conversionAttemptEx);}return convertedValue;}// 判断能够进行转换public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {Assert.notNull(targetType, "Target type to convert to cannot be null");if (sourceType == null) {return true;} else {GenericConverter converter = this.getConverter(sourceType, targetType);return converter != null;}}// GenericConversionService类里面的方法,具体判断逻辑@Nullableprotected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);// 先从缓存中获取GenericConverter converter = (GenericConverter)this.converterCache.get(key);if (converter != null) {return converter != NO_MATCH ? converter : null;} else {// 缓存中没有,然后进行判断逻辑converter = this.converters.find(sourceType, targetType);// 如果没有找到就给一个默认的if (converter == null) {converter = this.getDefaultConverter(sourceType, targetType);}// 放入缓存if (converter != null) {this.converterCache.put(key, converter);return converter;} else {this.converterCache.put(key, NO_MATCH);return null;}}}// GenericConversionService类里面的方法,这里就是具体如何找的,其实就是循环遍历,那个能够将sourceType请求带来的参数类型--》转换成targetType目标类型@Nullablepublic GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {List<Class<?>> sourceCandidates = this.getClassHierarchy(sourceType.getType());List<Class<?>> targetCandidates = this.getClassHierarchy(targetType.getType());Iterator var5 = sourceCandidates.iterator();// 两次循环遍历while(var5.hasNext()) {Class<?> sourceCandidate = (Class)var5.next();Iterator var7 = targetCandidates.iterator();while(var7.hasNext()) {Class<?> targetCandidate = (Class)var7.next();GenericConverter.ConvertiblePair convertiblePair = new GenericConverter.ConvertiblePair(sourceCandidate, targetCandidate);GenericConverter converter = this.getRegisteredConverter(sourceType, targetType, convertiblePair);if (converter != null) {return converter;}}}return null;}// GenericConversionService类里面的方法,@Nullableprivate GenericConverter getRegisteredConverter(TypeDescriptor sourceType, TypeDescriptor targetType, GenericConverter.ConvertiblePair convertiblePair) {// this.converters是一个map,通过convertiblePair为key(“java.lang.String -> java.lang.Integer”)获取转换器ConvertersForPair convertersForPair = (ConvertersForPair)this.converters.get(convertiblePair);if (convertersForPair != null) {// 这里其实就是获取了转换器,然后返回GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);if (converter != null) {return converter;}}Iterator var7 = this.globalConverters.iterator();GenericConverter globalConverter;do {if (!var7.hasNext()) {return null;}globalConverter = (GenericConverter)var7.next();} while(!((ConditionalConverter)globalConverter).matches(sourceType, targetType));return globalConverter;}
这里有124个转换器,会将http超文本协议传输过来的参数进行转换。
二、准备进行数据转换与绑定数据
// 开始进行参数解析@Nullablepublic final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");String name = ModelFactory.getNameForParameter(parameter);ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);if (ann != null) {mavContainer.setBinding(name, ann.binding());}Object attribute = null;BindingResult bindingResult = null;if (mavContainer.containsAttribute(name)) {attribute = mavContainer.getModel().get(name);} else {// 这里以上都不用管,以上是使用ModelAttribute注解处理的逻辑try {// 创建一个空的参数对象,也就是接口参数里面的自定义javaBean对象,但是里面的属性值是空的。之后就会让这个对象的属性值与请求进来所带的参数进行一一绑定。attribute = this.createAttribute(name, parameter, binderFactory, webRequest);} catch (BindException var10) {if (this.isBindExceptionRequired(parameter)) {throw var10;}if (parameter.getParameterType() == Optional.class) {attribute = Optional.empty();} else {attribute = var10.getTarget();}bindingResult = var10.getBindingResult();}}// 如果没有请求进来的参数进行绑定,就会进入下面进行绑定if (bindingResult == null) {// 创建绑定器,这里会把原生请求对象以及空属性person对象封装进去。同时还有各种类型的转换器,// 例如,所有请求都是通过http超文本协议传过来的,所以所有请求参数都是文本类型也就是String,比如年龄12,请求进来就会把"12"字符串类型转为Integer类型WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);if (binder.getTarget() != null) {if (!mavContainer.isBindingDisabled(name)) {// 这里就进行了参数绑定,也就是给Person绑定上了请求所携带的值。主要逻辑在里面this.bindRequestParameters(binder, webRequest);}this.validateIfApplicable(binder, parameter);if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {throw new BindException(binder.getBindingResult());}}if (!parameter.getParameterType().isInstance(attribute)) {attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);}bindingResult = binder.getBindingResult();}Map<String, Object> bindingResultModel = bindingResult.getModel();mavContainer.removeAttributes(bindingResultModel);mavContainer.addAllAttributes(bindingResultModel);return attribute;}// 这里会调用父类的方法创建一个Perrson对象protected final Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {String value = this.getRequestValueForAttribute(attributeName, request);if (value != null) {Object attribute = this.createAttributeFromRequestValue(value, attributeName, parameter, binderFactory, request);if (attribute != null) {return attribute;}}// 会进到这里return super.createAttribute(attributeName, parameter, binderFactory, request);}// 具体创建一个person对象逻辑protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {// 通过MethodParameter对象获取接口参数MethodParameter nestedParameter = parameter.nestedIfOptional();// 获取接口参数类型Class<?> clazz = nestedParameter.getNestedParameterType();// 获取参数类型对应的构造函数Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);// 这里就是通过底层反射之类的构造Perrson对象Object attribute = this.constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);if (parameter != nestedParameter) {attribute = Optional.of(attribute);}return attribute;}// 这里就进行了参数绑定,ServletModelAttributeMethodProcessor中的bindRequestParameters方法protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {// 获取请求ServletRequest servletRequest = (ServletRequest)request.getNativeRequest(ServletRequest.class);Assert.state(servletRequest != null, "No ServletRequest");ServletRequestDataBinder servletBinder = (ServletRequestDataBinder)binder;// 这里进行了数据绑定servletBinder.bind(servletRequest);}// 对应上面的bind数据绑定方法,ServletRequestDataBinder类里面的public void bind(ServletRequest request) {// 获取请求中参数的k-v数据对MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);MultipartRequest multipartRequest = (MultipartRequest)WebUtils.getNativeRequest(request, MultipartRequest.class);if (multipartRequest != null) {this.bindMultipart(multipartRequest.getMultiFileMap(), mpvs);} else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) {HttpServletRequest httpServletRequest = (HttpServletRequest)WebUtils.getNativeRequest(request, HttpServletRequest.class);if (httpServletRequest != null) {StandardServletPartUtils.bindParts(httpServletRequest, mpvs, this.isBindEmptyMultipartFiles());}}this.addBindValues(mpvs, request);// 这里是真正进行参数绑定的方法this.doBind(mpvs);}// WebDataBinder类里面的绑定方法protected void doBind(MutablePropertyValues mpvs) {this.checkFieldDefaults(mpvs);this.checkFieldMarkers(mpvs);this.adaptEmptyArrayIndices(mpvs);// 主要是调用父类的绑定方法super.doBind(mpvs);}// 父类DataBinder里面的绑定方法protected void doBind(MutablePropertyValues mpvs) {this.checkAllowedFields(mpvs);this.checkRequiredFields(mpvs);// 主要是这个方法this.applyPropertyValues(mpvs);}// 这个方法也是DataBinder里面的protected void applyPropertyValues(MutablePropertyValues mpvs) {try {// 设置属性值this.getPropertyAccessor().setPropertyValues(mpvs, this.isIgnoreUnknownFields(), this.isIgnoreInvalidFields());} catch (PropertyBatchUpdateException var7) {PropertyAccessException[] var3 = var7.getPropertyAccessExceptions();int var4 = var3.length;for(int var5 = 0; var5 < var4; ++var5) {PropertyAccessException pae = var3[var5];this.getBindingErrorProcessor().processPropertyAccessException(pae, this.getInternalBindingResult());}}}// 具体设置属性值的逻辑,AbstractPropertyAccessor这个类里面的方法public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException {List<PropertyAccessException> propertyAccessExceptions = null;// 获取所有需要设置进去的属性值,也就是请求形成的k-v对List<PropertyValue> propertyValues = pvs instanceof MutablePropertyValues ? ((MutablePropertyValues)pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues());if (ignoreUnknown) {this.suppressNotWritablePropertyException = true;}try {// 循环遍历数据Iterator var6 = propertyValues.iterator();while(var6.hasNext()) {PropertyValue pv = (PropertyValue)var6.next();try {// 设置属性值this.setPropertyValue(pv);} catch (NotWritablePropertyException var14) {if (!ignoreUnknown) {throw var14;}} catch (NullValueInNestedPathException var15) {if (!ignoreInvalid) {throw var15;}} catch (PropertyAccessException var16) {if (propertyAccessExceptions == null) {propertyAccessExceptions = new ArrayList();}propertyAccessExceptions.add(var16);}}} finally {if (ignoreUnknown) {this.suppressNotWritablePropertyException = false;}}if (propertyAccessExceptions != null) {PropertyAccessException[] paeArray = (PropertyAccessException[])propertyAccessExceptions.toArray(new PropertyAccessException[0]);throw new PropertyBatchUpdateException(paeArray);}}// 这段源码就是上面设置属性值的具体代码,AbstractNestablePropertyAccessor类里面的方法public void setPropertyValue(PropertyValue pv) throws BeansException {PropertyTokenHolder tokens = (PropertyTokenHolder)pv.resolvedTokens;if (tokens == null) {String propertyName = pv.getName();AbstractNestablePropertyAccessor nestedPa;try {nestedPa = this.getPropertyAccessorForPropertyPath(propertyName);} catch (NotReadablePropertyException var6) {throw new NotWritablePropertyException(this.getRootClass(), this.nestedPath + propertyName, "Nested property in path '" + propertyName + "' does not exist", var6);}tokens = this.getPropertyNameTokens(this.getFinalPath(nestedPa, propertyName));if (nestedPa == this) {pv.getOriginalPropertyValue().resolvedTokens = tokens;}// 这里是利用反射进行设置值的核心逻辑nestedPa.setPropertyValue(tokens, pv);} else {this.setPropertyValue(tokens, pv);}}// AbstractNestablePropertyAccessor类里面的方法protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {if (tokens.keys != null) {this.processKeyedProperty(tokens, pv);} else {this.processLocalProperty(tokens, pv);}}// 设置属性值的具体逻辑,AbstractNestablePropertyAccessor类里面的方法private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {PropertyHandler ph = this.getLocalPropertyHandler(tokens.actualName);if (ph != null && ph.isWritable()) {Object oldValue = null;PropertyChangeEvent propertyChangeEvent;try {// 拿到请求中具体的值,例如年龄 12等等Object originalValue = pv.getValue();Object valueToApply = originalValue;if (!Boolean.FALSE.equals(pv.conversionNecessary)) {if (pv.isConverted()) {valueToApply = pv.getConvertedValue();} else {if (this.isExtractOldValueForEditor() && ph.isReadable()) {try {oldValue = ph.getValue();} catch (Exception var8) {Exception ex = var8;if (var8 instanceof PrivilegedActionException) {ex = ((PrivilegedActionException)var8).getException();}if (logger.isDebugEnabled()) {logger.debug("Could not read previous value of property '" + this.nestedPath + tokens.canonicalName + "'", ex);}}}// 这里是最主要的,首先会进行请求参数的数据转换,例如将String类型的年龄转为Integer类型的年龄。。valueToApply = this.convertForProperty(tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());}// 到这里就完成了类型转换,pv.getOriginalPropertyValue().conversionNecessary = valueToApply != originalValue;}// 这里就将转换好的数据设置进value中,其他属性也是这样的步骤,这里是一个循环遍历设置的逻辑,循环代码在上面,这里的value就是12,name就是age,也就是在这里进行了数据绑定ph.setValue(valueToApply);} catch (TypeMismatchException var9) {throw var9;} catch (InvocationTargetException var10) {propertyChangeEvent = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());if (var10.getTargetException() instanceof ClassCastException) {throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), var10.getTargetException());} else {Throwable cause = var10.getTargetException();if (cause instanceof UndeclaredThrowableException) {cause = cause.getCause();}throw new MethodInvocationException(propertyChangeEvent, cause);}} catch (Exception var11) {propertyChangeEvent = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());throw new MethodInvocationException(propertyChangeEvent, var11);}} else if (pv.isOptional()) {if (logger.isDebugEnabled()) {logger.debug("Ignoring optional value for property '" + tokens.actualName + "' - property not found on bean class [" + this.getRootClass().getName() + "]");}} else if (!this.suppressNotWritablePropertyException) {throw this.createNotWritablePropertyException(tokens.canonicalName);}}// 数据转换器,AbstractNestablePropertyAccessor类里面的方法@Nullableprivate Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<?> requiredType, @Nullable TypeDescriptor td) throws TypeMismatchException {Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");PropertyChangeEvent pce;try {// 这里使用了代理,进入查看数据是如何转化的return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);} catch (IllegalStateException | ConverterNotFoundException var8) {pce = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + propertyName, oldValue, newValue);throw new ConversionNotSupportedException(pce, requiredType, var8);} catch (IllegalArgumentException | ConversionException var9) {pce = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + propertyName, oldValue, newValue);throw new TypeMismatchException(pce, requiredType, var9);}}// 数据转化具体逻辑,TypeConverterDelegate里面的方法@Nullablepublic <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);ConversionFailedException conversionAttemptEx = null;// 这里是获取124个转换器服务ConversionService conversionService = this.propertyEditorRegistry.getConversionService();if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);// 然后判断那个转换器能够进行转换if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {try {// 这里就开始进行数据转换了return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);} catch (ConversionFailedException var14) {conversionAttemptEx = var14;}}}Object convertedValue = newValue;if (editor != null || requiredType != null && !ClassUtils.isAssignableValue(requiredType, newValue)) {if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) && newValue instanceof String) {TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor();if (elementTypeDesc != null) {Class<?> elementType = elementTypeDesc.getType();if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {convertedValue = StringUtils.commaDelimitedListToStringArray((String)newValue);}}}if (editor == null) {editor = this.findDefaultEditor(requiredType);}convertedValue = this.doConvertValue(oldValue, convertedValue, requiredType, editor);}boolean standardConversion = false;if (requiredType != null) {if (convertedValue != null) {if (Object.class == requiredType) {return convertedValue;}if (requiredType.isArray()) {if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) {convertedValue = StringUtils.commaDelimitedListToStringArray((String)convertedValue);}return this.convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType());}if (convertedValue instanceof Collection) {convertedValue = this.convertToTypedCollection((Collection)convertedValue, propertyName, requiredType, typeDescriptor);standardConversion = true;} else if (convertedValue instanceof Map) {convertedValue = this.convertToTypedMap((Map)convertedValue, propertyName, requiredType, typeDescriptor);standardConversion = true;}if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) {convertedValue = Array.get(convertedValue, 0);standardConversion = true;}if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {return convertedValue.toString();}if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) {if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) {try {Constructor<T> strCtor = requiredType.getConstructor(String.class);return BeanUtils.instantiateClass(strCtor, new Object[]{convertedValue});} catch (NoSuchMethodException var12) {if (logger.isTraceEnabled()) {logger.trace("No String constructor found on type [" + requiredType.getName() + "]", var12);}} catch (Exception var13) {if (logger.isDebugEnabled()) {logger.debug("Construction via String failed for type [" + requiredType.getName() + "]", var13);}}}String trimmedValue = ((String)convertedValue).trim();if (requiredType.isEnum() && trimmedValue.isEmpty()) {return null;}convertedValue = this.attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue);standardConversion = true;} else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) {convertedValue = NumberUtils.convertNumberToTargetClass((Number)convertedValue, requiredType);standardConversion = true;}} else if (requiredType == Optional.class) {convertedValue = Optional.empty();}if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) {if (conversionAttemptEx != null) {throw conversionAttemptEx;}if (conversionService != null && typeDescriptor != null) {TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);}}StringBuilder msg = new StringBuilder();msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue));msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append("'");if (propertyName != null) {msg.append(" for property '").append(propertyName).append("'");}if (editor != null) {msg.append(": PropertyEditor [").append(editor.getClass().getName()).append("] returned inappropriate value of type '").append(ClassUtils.getDescriptiveType(convertedValue)).append("'");throw new IllegalArgumentException(msg.toString());}msg.append(": no matching editors or conversion strategy found");throw new IllegalStateException(msg.toString());}}if (conversionAttemptEx != null) {if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {throw conversionAttemptEx;}logger.debug("Original ConversionService attempt failed - ignored since PropertyEditor based conversion eventually succeeded", conversionAttemptEx);}return convertedValue;}// 开始进行数据转换,GenericConversionService类里面的方法// sourceType:源数据类型// targetType:目标数据类型// source:源数据@Nullablepublic Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {Assert.notNull(targetType, "Target type to convert to cannot be null");if (sourceType == null) {Assert.isTrue(source == null, "Source must be [null] if source type == [null]");return this.handleResult((TypeDescriptor)null, targetType, this.convertNullSource((TypeDescriptor)null, targetType));} else if (source != null && !sourceType.getObjectType().isInstance(source)) {throw new IllegalArgumentException("Source to convert from must be an instance of [" + sourceType + "]; instead it was a [" + source.getClass().getName() + "]");} else {GenericConverter converter = this.getConverter(sourceType, targetType);if (converter != null) {Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);return this.handleResult(sourceType, targetType, result);} else {return this.handleConverterNotFound(source, sourceType, targetType);}}}// 调用converter里面的convert方法进行数据转换,ConversionUtils这个类里面的@Nullablepublic static Object invokeConverter(GenericConverter converter, @Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {try {return converter.convert(source, sourceType, targetType);} catch (ConversionFailedException var5) {throw var5;} catch (Throwable var6) {throw new ConversionFailedException(sourceType, targetType, source, var6);}}// 会调用StringToNumberConverterFactory类下的这个方法进行转换public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {return new StringToNumber(targetType);} // 同一个类下的私有方法,StringToNumber对象,将String转为Number数据// 未来我们可以给WebDataBinder里面放自己的Converter;也就是说可以模仿这个类进行自定义转换,然后放入WebDataBinder绑定器中private static final class StringToNumber<T extends Number> implements Converter<String, T> {private final Class<T> targetType;public StringToNumber(Class<T> targetType) {this.targetType = targetType;}@Nullablepublic T convert(String source) {// 这里就是真正的转换return source.isEmpty() ? null : NumberUtils.parseNumber(source, this.targetType);}}// NumberUtils类下的方法,会根据你的目标是什么类型,进行解码public static <T extends Number> T parseNumber(String text, Class<T> targetClass) {Assert.notNull(text, "Text must not be null");Assert.notNull(targetClass, "Target class must not be null");String trimmed = StringUtils.trimAllWhitespace(text);if (Byte.class == targetClass) {return isHexNumber(trimmed) ? Byte.decode(trimmed) : Byte.valueOf(trimmed);} else if (Short.class == targetClass) {return isHexNumber(trimmed) ? Short.decode(trimmed) : Short.valueOf(trimmed);} else if (Integer.class == targetClass) {// 例如年龄是Integer,会进入到这里,将String 类型转为Ingeter类型,将String类型进行解码Integer.decode(trimmed)return isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed);} else if (Long.class == targetClass) {return isHexNumber(trimmed) ? Long.decode(trimmed) : Long.valueOf(trimmed);} else if (BigInteger.class == targetClass) {return isHexNumber(trimmed) ? decodeBigInteger(trimmed) : new BigInteger(trimmed);} else if (Float.class == targetClass) {return Float.valueOf(trimmed);} else if (Double.class == targetClass) {return Double.valueOf(trimmed);} else if (BigDecimal.class != targetClass && Number.class != targetClass) {throw new IllegalArgumentException("Cannot convert String [" + text + "] to target class [" + targetClass.getName() + "]");} else {return new BigDecimal(trimmed);}}
三、自定义类型转换器
测试用例中,访问接口进行传参,接口参数中的Pet对象,里面的属性是这样传递的。pet.name=xxx以及pet.age = 12.如果我们缓存pet = 小猫,12;其中小猫是name的值,12是age的值,这样springmvc就没法进行类型绑定,这是由于WebDataBinder 中的Converters没办法进行将请求参数与javabean进行类型绑定。所以需要我们自定义类型转换器。进行数据类型转换,然后绑定。
一、自定义类型转换器
// 类型转换函数表达式,S表示请求进来的参数,T表示转换成的目标参数
@FunctionalInterface
public interface Converter<S, T> {@NullableT convert(S var1);default <U> Converter<S, U> andThen(Converter<? super T, ? extends U> after) {Assert.notNull(after, "After Converter must not be null");return (s) -> {T initialResult = this.convert(s);return initialResult != null ? after.convert(initialResult) : null;};}
}
需要在spring容器中注入自定义的类型转换器。
@Configuration
public class SpringMvcConfig {/**WebMvcConfigurer可以定制化springmvc功能* 这个接口里面有一个方法,添加类型转化器以及格式化器(比如说日期格式化)* default void addFormatters(FormatterRegistry registry) {* }* 这里添加请求参数转换Person中Pet属性的转换器,只要是String-》Person.Pet对象的都是走这个转换器* @return*/@Beanpublic WebMvcConfigurer webMvcConfigurer(){return new WebMvcConfigurer() {@Overridepublic void addFormatters(FormatterRegistry registry) {registry.addConverter(new Converter<String, Person.Pet>() {@Overridepublic Person.Pet convert(String source) {if (!StringUtils.hasLength(source)){return null;}String[] split = source.split(",");Person.Pet pet = new Person.Pet();pet.setName(split[0]);pet.setAge(split[1]);return pet;}});}};}}
相关文章:

springboot系列--web相关知识探索五
一、前言 web相关知识探索四中研究了请求中所带的参数是如何映射到接口参数中的,也即请求参数如何与接口参数绑定。主要有四种、分别是注解方式、Servlet API方式、复杂参数、以及自定义对象参数。web相关知识探索四中主要研究了复杂参数底层绑定原理。本次主要是研…...

开源商城系统crmeb phpstudy安装配置
BOSS让我最快时间部署一套开源商场系统,今天就以crmeb为例。 快速部署在linux中我会首选docker,因为我要在windows中部署,本文就选用phpstudy集成环境做了。 什么是crmeb 我从官网摘点: CRMEB产品与服务 CRMEB通过将CRM&#x…...

【论文阅读笔记】Bigtable: A Distributed Storage System for Structured Data
文章目录 1 简介2 数据模型2.1 行2.2 列族2.3 时间戳 3 API4 基础构建4.1 GFS4.2 SSTable4.3 Chubby 5 实现5.1 Tablet 位置5.2 Tablet 分配5.3 为 tablet 提供服务5.4 压缩5.4.1 小压缩5.4.2 主压缩 6 优化6.1 局部性组6.2 压缩6.3 缓存6.4 布隆过滤器6.5 Commit日志实现6.6 T…...

linux从入门到精通-从基础学起,逐步提升,探索linux奥秘(十一)--rpm管理和计划任务
linux从入门到精通-从基础学起,逐步提升,探索linux奥秘(十一)–rpm管理和计划任务 一、rpm管理(重点) 1、rpm管理 作用: rpm的作用类似于windows上的电脑管家中“软件管理”、安全卫士里面“…...

【C++几种单例模式解读及实现方式】
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、单例是什么?二、解读1.懒汉式2.饿汉式3.static变量特性4.call_once特性 总结 前言 单例模式几乎是每种语言都不可少的一种设计模式,…...

QT开发--串口通信
第十六章 串口通信 16.1 串口通信基础 串口通信主要通过DB9接口,适用于短距离(<10米)。关键参数包括: 波特率:每秒传输bit数,如9600。数据位:信息包中的有效数据位数。停止位:…...

数据库(至少还的再花两天 )
1 连接查询 左连接 右连接 2 聚合函数 SQL 统计求和 求最值 count sum avg max min 3 SQL关键字 limit 分页 group by 分组 distinct 去重 4 Select执行顺序 from where group by order by 5 数据库三范式 原子性 唯一性 直接性 6 存储引擎 MyISAM InnoDB 7 …...

网络安全公司及其主要产品介绍
以下是一些全球领先的网络安全公司及其主要产品介绍: 一、思科(Cisco) 思科是全球最大的网络设备供应商之一,其网络安全产品以企业级解决方案为主,覆盖多种安全需求。 Cisco ASA(Adaptive Security Appli…...

orjson:高性能的Python JSON库
在Python中处理JSON数据是一项常见任务,标准库的json模块虽然功能齐全,但在性能方面还有提升空间。今天我要向大家介绍一个出色的第三方JSON库 - orjson。 orjson简介 orjson是一个快速、正确的Python JSON库。它具有以下主要特点: 性能卓越 - 在序列化和反序列化方面都比标准…...

常见几大排序算法
排序算法是计算机科学中的基本算法,它们将一个无序的数组或列表按特定顺序进行排列(如升序或降序)。常见的排序算法可以根据其时间复杂度、空间复杂度和适用场景分类。以下是几种常见的排序算法: 1. 冒泡排序(Bubble …...

Linux下CMake入门
CMake的基础知识 什么是 CMake CMake 是一个跨平台的构建工具,主要用于管理构建过程。CMake 不直接构建项目,而是生成特定平台上的构建系统(如 Unix 下的 Makefile,Windows 下的 Visual Studio 工程),然后…...

网络资源模板--Android Studio 实现简易记事本App
目录 一、项目演示 二、项目测试环境 三、项目详情 四、完整的项目源码 一、项目演示 网络资源模板--基于Android studio 实现的简易记事本App 二、项目测试环境 三、项目详情 首页 创建一个空的笔记本列表 mNotebookList。使用该列表和指定的布局资源 item_notebook 创建…...

根据Vue对比来深入学习React 下 props 组件传值 插槽 样式操作 hooks 高阶组件 性能优化
文章目录 函数组件的特点props组件间的传值父传子看上例子传父兄弟组件传值祖先组件传值 插槽基础插槽具名插槽作用域插槽 样式操作**CSS Modules** 生命周期useRef常用hookuseStateuseEffectuseContextuseReduceruseMemouseCallback 高阶组件什么时候使用 react性能问题和优化…...

HTML(六)超链接
HTML讲解(一)body部分_html body-CSDN博客 <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>title</title> </head><body><a href"https://blog.csdn.net/2301_8034953…...

【Coroutines】Implement Lua Coroutine by Kotlin - 2
Last Chapter Link 文章目录 Symmetric CoroutinesNon-Symmetric Coroutine SampleSymmetric Coroutine SampleHow to Implement Symmetric CoroutinesWonderful TricksCode DesignTail Recursion OptimizationFull Sources Symmetric Coroutines in last blog, we have talk…...

java计算机毕设课设—扫雷游戏(附源码、文章、相关截图、部署视频)
这是什么系统? 资源获取方式再最下方(本次10月份活动福利,免费提供下载,自行到对应的方式1下载,csdn的0积分下载) java计算机毕设课设—扫雷游戏(附源码、文章、相关截图、部署视频) 基于Java的扫雷游戏…...

AndroidLogger 使用问题
Q1:解压zip后,启动Notepad未看到AndroidLogger工具栏 请检查plugins下安装位置是否正确,必须与下图一致,再确认Notepad 是否为 x64 ? Q2:使用 adb 可以显示已连接,但是获取不到日志 暂时不确定问…...

数据库常见面试
8道面试题 目录 目录 7道面试题 1.怎样进行sql优化 4、group by优化 5、limit优化 6、count优化 7、update优化 2.。怎样查看sql执行情况呢(哪个关键字),说说你对这个关键字的认识 4) possible_key: 5) key 3.说说你对innodb和 myisam的理解 …...

boxplot 绘制箱线图,添加数据点
先看效果图 import matplotlib.pyplot as plt #! 解决不显示的问题:中文设置为宋体格式 plt.rcParams[font.family] ["Times New Roman", SimSun]def plot_boxplot(data_list, out_file, x_custom_labels):# 画图fig, ax plt.subplots(figsize(90, 6…...

用sdkman管理多个jdk切换
前言 最近项目前后端进行升级,需要在jdk8和jdk17两个版本切换。最简单的是通过手动切换,但切换过程太繁琐,修改环境变量,达到切换目的。于是尝试其它解决方案,最终确实使用sdkman工具。 sdkman 是一款面向Java开发者的…...

【AIGC】ChatGPT提示词Prompt高效编写模式:结构化Prompt、提示词生成器与单样本/少样本提示
💯前言 在如今AI技术迅猛发展的背景下,尽管像ChatGPT这样的大型语言模型具备强大的生成能力,但它们的输出质量有时仍难以完全满足我们的预期。为了让ChatGPT生成更加准确、可靠的内容,掌握高效的Prompt编写技巧变得尤为重要。本文…...

反调式实战(有道翻译窗口弹出)
1.添加脚本断点实现源码获取 2.Function构造器构造debugger 因为是窗口被弹出的情况,所以window.closefunction()构造debugger。 3.定位到影响弹出的JavaScript代码片段 反调试思想:置空和替换,所以将其JavaScript进行注释或者删除。 这里主…...

verilog端口使用注意事项
下图存在组合逻辑反馈环,即组合逻辑的输出反馈到输入(赋值的左右2边存在相同的信号),此种情况会造成系统不稳定。比如在data_in20的情况下,在data_out0 时候,输出的数据会反馈到输入,输入再输出,从而造成不…...

Docker常用命令大全汇总
Docker是一种流行的容器化平台,可以在一个独立的、隔离的环境中构建、部署和运行应用程序。了解Docker常用命令可以帮助我们更高效地管理容器,快速开发和部署应用。本文将整理一系列Docker的常用命令,便于日常使用和学习。 1 Docker基础命令 1.1 启动/停止/重启docker # …...

LVS-DR+Keepalived 高可用群集部署
LVS-DRKeepalived 高可用群集部署 Keepalived 的工作原理LVSKeepalived 高可用群集部署配置负载调度器(主、备相同)关闭防火墙和核心防护及准备IPVS模块配置keeplived(主、备DR 服务器上都要设置)启动 ipvsadm 服务调整 proc 响应…...

【elasticsearch】安装和启动
启动 Elasticsearch 并验证其是否成功运行通常涉及以下步骤: 下载和安装 Elasticsearch: 访问 Elasticsearch 官方网站下载页面:https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html根据你的操作系…...

Golang 逃逸分析(Escape Analysis)理解与实践篇
Golang 逃逸分析(Escape Analysis)理解与实践篇 文章目录 1.逃逸分析2.相关知识(栈、堆、GC分析)3.逃逸分析综合-实践 demo 逃逸分析(Escape Analysis)是编译器在编译期进行的一项优化技术,是Gl…...

React入门 9:React Router
1. 什么是路由 路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。 以上是中文维基百科对路由的解释。通俗的来讲,把一个地方的信息传输到他想去的目的地的过程,就叫路由。 2. 用代码解释路由 需求:…...

MATLAB基础应用精讲-【数模应用】Bland-Altman图(附python和R语言代码实现)
目录 前言 几个高频面试题目 Bland-altman图:如何改变y轴 算法原理 Bland-Altman一致性分析 一致性界限 1. 背景介绍 2. Bland-Altman 法 3. batplot 命令介绍 4. 应用实例 Prism GraphPad实现Bland-Altman图 1.输入数据 2.从数据表中选择Bland-Altman分析 3.检…...

ARM/Linux嵌入式面经(四一):中兴面经
1. 请介绍一下您在嵌入式系统开发中的项目经验。 在嵌入式系统开发领域,我积累了丰富的项目经验,这些经验不仅锻炼了我的技术能力,也让我对嵌入式系统的设计和实现有了更深入的理解。以下是我参与的一个具有代表性的嵌入式系统开发项目的详细介绍: 项目背景 该项目是为一…...