每篇一句

我们应该做一个:胸中有蓝图,脚底有计划的人

前言

Spring MVC提供的基于注释的编程模型,极大的简化了web应用的开发,我们都是受益者。比如我们在@RestController标注的Controller控制器组件上用@RequestMapping@ExceptionHandler等注解来表示请求映射、异常处理等等。

使用这种注解的方式来开发控制器我认为最重要的优势是:

  1. 灵活的方法签名(入参随意写)
  2. 不必继承基类
  3. 不必实现接口

总之一句话:灵活性非常强,耦合度非常低。

在众多的注解使用中,Spring MVC中有一个非常强大但几乎被忽视的一员:@ModelAttribute。关于这个注解的使用情况,我在群里/线下问了一些人,感觉很少人会使用这个注解(甚至有的不知道有这个注解),这着实让我非常的意外。我认为至少这对于"久经战场"的一个老程序员来说这是不应该的吧。

不过没关系,有幸看到此文,能够帮你弥补弥补这块的盲区。

@ModelAttribute它不是开发必须的注解(不像@RequestMapping那么重要),so即使你不知道它依旧能正常书写控制器。当然,正所谓没有最好只有更好,倘若你掌握了它,便能够帮助你更加高效的写代码,让你的代码复用性更强、代码更加简洁、可维护性更高。

这种知识点就像反射、就像内省,即使你不知道它你完全也可以工作、写业务需求。但是若你能够熟练使用,那你的可想象空间就会更大了,未来可期。虽然它不是必须,但是它是个很好的辅助~

@ModelAttribute官方解释

首先看看Spring官方的JavaDoc对它怎么说:它将方法参数/方法返回值绑定到web viewModel里面。只支持@RequestMapping这种类型的控制器哦。它既可以标注在方法入参上,也可以标注在方法(返回值)上。

但是请注意,当请求处理导致异常时,引用数据和所有其他模型内容对Web视图不可用,因为该异常随时可能引发,使Model内容不可靠。因此,标注有@Exceptionhandler的方法不提供对Model参数的访问~

// @since 2.5  只能用在入参、方法上
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute { @AliasFor("name")
String value() default "";
// The name of the model attribute to bind to. 注入如下默认规则
// 比如person对应的类是:mypackage.Person(类名首字母小写)
// personList对应的是:List<Person> 这些都是默认规则咯~~~ 数组、Map的省略
// 具体可以参考方法:Conventions.getVariableNameForParameter(parameter)的处理规则
@AliasFor("value")
String name() default ""; // 若是false表示禁用数据绑定。
// @since 4.3
boolean binding() default true;
}

基本原理

我们知道@ModelAttribute能标注在入参上,也可以标注在方法上。下面就从原理处深入理解,从而掌握它的使用,后面再给出多种使用场景的使用Demo

和它相关的两个类是ModelFactoryModelAttributeMethodProcessor

@ModelAttribute缺省处理的是Request请求域,Spring MVC还提供了@SessionAttributes来处理和Session域相关的模型数据,详见:从原理层面掌握@SessionAttributes的使用【一起学Spring MVC】

关于ModelFactory的介绍,在这里讲解@SessionAttributes的时候已经介绍一大部分了,但特意留了一部分关于@ModelAttribute的内容,在本文继续讲解

ModelFactory

ModelFactory所在包org.springframework.web.method.annotation,可见它和web是强关联的在一起的。作为上篇文章的补充说明,接下里只关心它对@ModelAttribute的解析部分:

// @since 3.1
public final class ModelFactory { // 初始化Model 这个时候`@ModelAttribute`有很大作用
public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
// 拿到sessionAttr的属性
Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
// 合并进容器内
container.mergeAttributes(sessionAttributes);
// 这个方法就是调用执行标注有@ModelAttribute的方法们~~~~
invokeModelAttributeMethods(request, container);
...
} //调用标注有注解的方法来填充Model
private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container) throws Exception {
// modelMethods是构造函数进来的 一个个的处理吧
while (!this.modelMethods.isEmpty()) {
// getNextModelMethod:通过next其实能看出 执行是有顺序的 拿到一个可执行的InvocableHandlerMethod
InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod(); // 拿到方法级别的标注的@ModelAttribute~~
ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class);
Assert.state(ann != null, "No ModelAttribute annotation");
if (container.containsAttribute(ann.name())) {
if (!ann.binding()) { // 若binding是false 就禁用掉此name的属性 让不支持绑定了 此方法也处理完成
container.setBindingDisabled(ann.name());
}
continue;
} // 调用目标的handler方法,拿到返回值returnValue
Object returnValue = modelMethod.invokeForRequest(request, container);
// 方法返回值不是void才需要继续处理
if (!modelMethod.isVoid()){ // returnValueName的生成规则 上文有解释过 本处略
String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
if (!ann.binding()) { // 同样的 若禁用了绑定,此处也不会放进容器里
container.setBindingDisabled(returnValueName);
} //在个判断是个小细节:只有容器内不存在此属性,才会放进去 因此并不会有覆盖的效果哦~~~
// 所以若出现同名的 请自己控制好顺序吧
if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue);
}
}
}
} // 拿到下一个标注有此注解方法~~~
private ModelMethod getNextModelMethod(ModelAndViewContainer container) { // 每次都会遍历所有的构造进来的modelMethods
for (ModelMethod modelMethod : this.modelMethods) {
// dependencies:表示该方法的所有入参中 标注有@ModelAttribute的入参们
// checkDependencies的作用是:所有的dependencies依赖们必须都是container已经存在的属性,才会进到这里来
if (modelMethod.checkDependencies(container)) {
// 找到一个 就移除一个
// 这里使用的是List的remove方法,不用担心并发修改异常??? 哈哈其实不用担心的 小伙伴能知道为什么吗??
this.modelMethods.remove(modelMethod);
return modelMethod;
}
} // 若并不是所有的依赖属性Model里都有,那就拿第一个吧~~~~
ModelMethod modelMethod = this.modelMethods.get(0);
this.modelMethods.remove(modelMethod);
return modelMethod;
}
...
}

ModelFactory这部分做的事:执行所有的标注有@ModelAttribute注解的方法,并且是顺序执行哦。那么问题就来了,这些handlerMethods是什么时候被“找到”的呢???这个时候就来到了RequestMappingHandlerAdapter,来看看它是如何找到这些标注有此注解@ModelAttribute的处理器的~~~

RequestMappingHandlerAdapter

RequestMappingHandlerAdapter是个非常庞大的体系,本处我们只关心它对@ModelAttribute也就是对ModelFactory的创建,列出相关源码如下:

//  @since 3.1
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { // 该方法不能标注有@RequestMapping注解,只标注了@ModelAttribute才算哦~
public static final MethodFilter MODEL_ATTRIBUTE_METHODS = method ->
(!AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class) && AnnotatedElementUtils.hasAnnotation(method, ModelAttribute.class));
...
// 从Advice里面分析出来的标注有@ModelAttribute的方法(它是全局的)
private final Map<ControllerAdviceBean, Set<Method>> modelAttributeAdviceCache = new LinkedHashMap<>(); @Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 每调用一次都会生成一个ModelFactory ~~~
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
...
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
// 初始化Model
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
...
return getModelAndView(mavContainer, modelFactory, webRequest);
} // 创建出一个ModelFactory,来管理Model
// 显然和Model相关的就会有@ModelAttribute @SessionAttributes等注解啦~
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
// 从缓存中拿到和此Handler相关的SessionAttributesHandler处理器~~处理SessionAttr
SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
Class<?> handlerType = handlerMethod.getBeanType(); // 找到当前类(Controller)所有的标注的@ModelAttribute注解的方法
Set<Method> methods = this.modelAttributeCache.get(handlerType);
if (methods == null) {
methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
this.modelAttributeCache.put(handlerType, methods);
} List<InvocableHandlerMethod> attrMethods = new ArrayList<>();
// Global methods first
// 全局的有限,最先放进List最先执行~~~~
this.modelAttributeAdviceCache.forEach((clazz, methodSet) -> {
if (clazz.isApplicableToBeanType(handlerType)) {
Object bean = clazz.resolveBean();
for (Method method : methodSet) {
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
}
});
for (Method method : methods) {
Object bean = handlerMethod.getBean();
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
} // 构造InvocableHandlerMethod
private InvocableHandlerMethod createModelAttributeMethod(WebDataBinderFactory factory, Object bean, Method method) {
InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(bean, method);
if (this.argumentResolvers != null) {
attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
attrMethod.setDataBinderFactory(factory);
return attrMethod;
}
}

RequestMappingHandlerAdapter这部分处理逻辑:每次请求过来它都会创建一个ModelFactory,从而收集到全局的(来自@ControllerAdvice)+ 本Controller控制器上的所有的标注有@ModelAttribute注解的方法们。

@ModelAttribute标注在单独的方法上(木有@RequestMapping注解),它可以在每个控制器方法调用之前,创建出一个ModelFactory从而管理Model数据~

ModelFactory管理着Model,提供了@ModelAttribute以及@SessionAttributes等对它的影响

同时@ModelAttribute可以标注在入参、方法(返回值)上的,标注在不同地方处理的方式是不一样的,那么接下来又一主菜ModelAttributeMethodProcessor就得登场了。

ModelAttributeMethodProcessor

从命名上看它是个Processor,所以根据经验它既能处理入参,也能处理方法的返回值:HandlerMethodArgumentResolver + HandlerMethodReturnValueHandler。解析@ModelAttribute注解标注的方法参数,并处理@ModelAttribute标注的方法返回值。

先看它对方法入参的处理(稍显复杂):

// 这个处理器用于处理入参、方法返回值~~~~
// @since 3.1
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private final boolean annotationNotRequired; public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
this.annotationNotRequired = annotationNotRequired;
} // 入参里标注了@ModelAttribute 或者(注意这个或者) annotationNotRequired = true并且不是isSimpleProperty()
// isSimpleProperty():八大基本类型/包装类型、Enum、Number等等 Date Class等等等等
// 所以划重点:即使你没标注@ModelAttribute 单子还要不是基本类型等类型,都会进入到这里来处理
// 当然这个行为是是收到annotationNotRequired属性影响的,具体的具体而论 它既有false的时候 也有true的时候
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
} // 说明:能进入到这里来的 证明入参里肯定是有对应注解的???
// 显然不是,上面有说 这事和属性值annotationNotRequired有关的~~~
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 拿到ModelKey名称~~~(注解里有写就以注解的为准)
String name = ModelFactory.getNameForParameter(parameter);
// 拿到参数的注解本身
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
} Object attribute = null;
BindingResult bindingResult = null; // 如果model里有这个属性,那就好说,直接拿出来完事~
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
} else { // 若不存在,也不能让是null呀
// Create attribute instance
// 这是一个复杂的创建逻辑:
// 1、如果是空构造,直接new一个实例出来
// 2、若不是空构造,支持@ConstructorProperties解析给构造赋值
// 注意:这里就支持fieldDefaultPrefix前缀、fieldMarkerPrefix分隔符等能力了 最终完成获取一个属性
// 调用BeanUtils.instantiateClass(ctor, args)来创建实例
// 注意:但若是非空构造出来,是立马会执行valid校验的,此步骤若是空构造生成的实例,此步不会进行valid的,但是下一步会哦~
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
} catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
} // 若是空构造创建出来的实例,这里会进行数据校验 此处使用到了((WebRequestDataBinder) binder).bind(request); bind()方法 唯一一处
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
// 绑定request请求数据
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
// 执行valid校验~~~~
validateIfApplicable(binder, parameter);
//注意:此处抛出的异常是BindException
//RequestResponseBodyMethodProcessor抛出的异常是:MethodArgumentNotValidException
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
} // Add resolved attribute and BindingResult at the end of the model
// at the end of the model 把解决好的属性放到Model的末尾~~~
// 可以即使是标注在入参上的@ModelAtrribute的属性值,最终也都是会放进Model里的~~~可怕吧
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel); return attribute;
} // 此方法`ServletModelAttributeMethodProcessor`子类是有复写的哦~~~~
// 使用了更强大的:ServletRequestDataBinder.bind(ServletRequest request)方法
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
((WebRequestDataBinder) binder).bind(request);
}
}

模型属性首先从Model中获取,若没有获取到,就使用默认构造函数(可能是有无参,也可能是有参)创建,然后会把ServletRequest请求的数据绑定上来, 然后进行@Valid校验(若添加有校验注解的话),最后会把属性添加到Model里面

最后加进去的代码是:mavContainer.addAllAttributes(bindingResultModel);这里我贴出参考值:



如下示例,它会正常打印person的值,而不是null(因为Model内有person了~)

请求链接是:/testModelAttr?name=wo&age=10

    @GetMapping("/testModelAttr")
public void testModelAttr(@Valid Person person, ModelMap modelMap) {
Object personAttr = modelMap.get("person");
System.out.println(personAttr); //Person(name=wo, age=10)
}

注意:虽然person上没有标注@ModelAtrribute,但是modelMap.get("person")依然是能够获取到值的哦,至于为什么,原因上面已经分析了,可自行思考。


下例中:

    @GetMapping("/testModelAttr")
public void testModelAttr(Integer age, Person person, ModelMap modelMap) {
System.out.println(age); // 直接封装的值
System.out.println("-------------------------------");
System.out.println(modelMap.get("age"));
System.out.println(modelMap.get("person"));
}

请求:/testModelAttr?name=wo&age=10 输入为:

10
-------------------------------
null
Person(name=wo, age=10)

可以看到普通类型(注意理解这个普通类型)若不标注@ModelAtrribute,它是不会自动识别为Model而放进来的哟~~~若你这么写:

    @GetMapping("/testModelAttr")
public void testModelAttr(@ModelAttribute("age") Integer age, Person person, ModelMap modelMap) {
System.out.println(age); // 直接封装的值
System.out.println("-------------------------------");
System.out.println(modelMap.get("age"));
System.out.println(modelMap.get("person"));
}

打印如下:

10
-------------------------------
10
Person(name=wo, age=10)

请务必注意以上case的区别,加深记忆。使用的时候可别踩坑了~


再看它对方法(返回值)的处理(很简单):

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

	// 方法返回值上标注有@ModelAttribute注解(或者非简单类型)  默认都会放进Model内哦~~
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
} // 这个处理就非常非常的简单了,注意:null值是不放的哦~~~~
// 注意:void的话 returnValue也是null
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue != null) {
String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
mavContainer.addAttribute(name, returnValue);
}
}
}

它对方法返回值的处理非常简单,只要不是null(当然不能是void)就都会放进Model里面,供以使用

总结

本文介绍的是@ModelAttribute的核心原理,他对我们实际使用有重要的理论支撑。下面系列文章主要在原理的基础上,展示各种各样场景下的使用Demo,敬请关注~

相关阅读

从原理层面掌握@SessionAttributes的使用【一起学Spring MVC】

从原理层面掌握@RequestAttribute、@SessionAttribute的使用【一起学Spring MVC】

从原理层面掌握@ModelAttribute的使用(使用篇)【一起学Spring MVC】

知识交流

The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~

若对技术内容感兴趣可以加入wx群交流:Java高工、架构师3群

若群二维码失效,请加wx号:fsx641385712(或者扫描下方wx二维码)。并且备注:"java入群" 字样,会手动邀请入群

若文章格式混乱或者图片裂开,请点击`:原文链接-原文链接-原文链接

从原理层面掌握@ModelAttribute的使用(核心原理篇)【一起学Spring MVC】的更多相关文章

  1. 从原理层面掌握@RequestAttribute、@SessionAttribute的使用【一起学Spring MVC】

    每篇一句 改我们就改得:取其精华,去其糟粕.否则木有意义 前言 如果说知道@SessionAttributes这个注解的人已经很少了,那么不需要统计我就可以确定的说:知道@RequestAttribu ...

  2. 从原理层面掌握@ModelAttribute的使用(使用篇)【一起学Spring MVC】

    每篇一句 每个人都应该想清楚这个问题:你是祖师爷赏饭吃的,还是靠老天爷赏饭吃的 前言 上篇文章 描绘了@ModelAttribute的核心原理,这篇聚焦在场景使用上,演示@ModelAttribute ...

  3. 从原理层面掌握@SessionAttribute的使用【一起学Spring MVC】

    每篇一句 不是你当上了火影大家就认可你,而是大家都认可你才能当上火影 前言 该注解顾名思义,作用是将Model中的属性同步到session会话当中,方便在下一次请求中使用(比如重定向场景~). 虽然说 ...

  4. Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】

    每篇一句 在绝对力量面前,一切技巧都是浮云 前言 上文 介绍了Http内容协商的一些概念,以及Spring MVC内置的4种协商方式使用介绍.本文主要针对Spring MVC内容协商方式:从步骤.原理 ...

  5. RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】

    每篇一句 人圆月圆心圆,人和家和国和---中秋节快乐 前言 在阅读本篇之前,建议先阅读开山篇效果更佳.RestTemplate是Spring提供的用于访问Rest服务的客户端工具,它提供了多种便捷访问 ...

  6. 从原理层面掌握@InitBinder的使用【享学Spring MVC】

    每篇一句 大魔王张怡宁:女儿,这堆金牌你拿去玩吧,但我的银牌不能给你玩.你要想玩银牌就去找你王浩叔叔吧,他那银牌多 前言 为了讲述好Spring MVC最为复杂的数据绑定这块,我前面可谓是做足了功课, ...

  7. 从原理层面掌握HandlerMethod、InvocableHandlerMethod、ServletInvocableHandlerMethod的使用【一起学Spring MVC】

    每篇一句 想当火影的人没有近道可寻,当上火影的人同样无路可退 前言 HandlerMethod它作为Spring MVC的非公开API,可能绝大多数小伙伴都对它比较陌生,但我相信你对它又不是那么的生疏 ...

  8. (4.1)Spring MVC执行原理和基于Java的配置过程

    一.Spring MVC执行原理和基于Java配置的配置过程 (一)Spring MVC执行过程,大致为7步. 所有的请求都会经过Spring的一个单例的DispacherServlet. Dispa ...

  9. Spring MVC执行原理和基于Java的配置过程

    一.Spring MVC执行原理和基于Java配置的配置过程 (一)Spring MVC执行过程,大致为7步. 所有的请求都会经过Spring的一个单例的DispacherServlet. Dispa ...

随机推荐

  1. Django rest framework(1)----认证

    目录 Django组件库之(一) APIView源码 Django restframework   (1)  ----认证 Django rest framework(2)----权限 Django ...

  2. [Noi2002]Savage 题解

    [Noi2002]Savage 时间限制: 5 Sec  内存限制: 64 MB 题目描述 输入 第1行为一个整数N(1<=N<=15),即野人的数目. 第2行到第N+1每行为三个整数Ci ...

  3. web安全测试必须注意的五个方面

    随着互联网的飞速发展,web应用在软件开发中所扮演的角色变得越来越重要,同时,web应用遭受着格外多的安全攻击,其原因在于,现在的网站以及在网站上运行的应用在某种意义上来说,它是所有公司或者组织的虚拟 ...

  4. 实验吧--web--天下武功唯快不破

    ---恢复内容开始--- 英文翻译过来嘛,就是:天下武功无快不破嘛.(出题者还是挺切题的) 看看前端源码: 注意这里 please post what you find with parameter: ...

  5. 个人用户永久免费,可自动升级版Excel插件,使用VSTO开发,Excel催化剂安装过程详解及安装失败解决方法

    因Excel催化剂用了VSTO的开发技术,并且为了最好的用户体验,用了Clickonce的布署方式(无需人工干预自动更新,让用户使用如浏览器访问网站一般,永远是最新的内容和功能).对安装过程有一定的难 ...

  6. 个人永久性免费-Excel催化剂功能第48波-拆分工作薄内工作表,堪称Excel界的单反

    一个工作薄有多个相同类型的工作表,然后想通过批量操作,把每个工作表都另存为一个工作薄文件,这个批量拆分工作薄,绝大多数插件都有此功能,就如懂点VBA的高级用户也常常有点不屑于用插件来完成,自己写向行V ...

  7. markdown插入表格语法

    markdown插入表格语法 举例 如表格标题为,姓名,班级,成绩 标题内的内容为,yang,a班,100 我们要在markdow文件中插入表格 如 姓名|班级|成绩 -|-|- yang|a班|10 ...

  8. [leetcode] 96 Unique Binary Search Trees (Medium)

    原题 字母题 思路: 一开始妹有一点思路,去查了二叉查找树,发现有个叫做卡特兰数的东西. 1.求可行的二叉查找树的数量,只要满足中序遍历有序. 2.以一个结点为根的可行二叉树数量就是左右子树可行二叉树 ...

  9. 什么是icmp协议?

    英文原义:Internet Control Message Protocol 中文释义:(RFC-792)Internet控制消息协议 定义:      ICMP协议是一种面向无连接的协议,用于传输出 ...

  10. [NLP-ASR] 语音识别项目整理(一) 语音预处理

      简介 之前参与过114对话系统的项目,中间搁置很久,现在把之前做过的内容整理一下,一是为自己回顾,二是也希望分享自己看的内容,中间也遇到一些问题,如果您可以提一些建议将不胜感激. 114查询主要分 ...