SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段。在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过service()方法,委派到doGet()或者doPost()这些方法,完成Http请求的处理。

在初始化流程中,SpringMVC巧妙的运用依赖注入读取参数,并最终建立一个与容器上下文相关联的Spring子上下文。这个子上下文,就像Struts2中xwork容器一样,为接下来的Http处理流程中各种编程元素提供了容身之所。如果说将Spring上下文关联到Servlet容器中,是SpringMVC框架的第一个亮点,那么在请求转发流程中,SpringMVC对各种处理环节编程元素的抽象,就是另外一个独具匠心的亮点。

Struts2采取的是一种完全和Web容器隔离和解耦的事件机制。诸如Action对象、Result对象、Interceptor对象,这些都是完全脱离Servlet容器的编程元素。Struts2将数据流和事件处理完全剥离开来,从Http请求中读取数据后,下面的事件处理流程就只依赖于这些数据,而完全不知道有Web环境的存在。

反观SpringMVC,无论HandlerMapping对象、HandlerAdapter对象还是View对象,这些核心的接口所定义的方法中,HttpServletRequest和HttpServletResponse对象都是直接作为方法的参数出现的。这也就意味着,框架的设计者,直接将SpringMVC框架和容器绑定到了一起。或者说,整个SpringMVC框架,都是依托着Servlet容器元素来设计的。下面就来看一下,源码中是如何体现这一点的。

1.请求转发的入口

就像任何一个注册在容器中的Servlet一样,DispatcherServlet也是通过自己的service()方法来接收和转发Http请求到具体的doGet()或doPost()这些方法的。以一次典型的GET请求为例,经过HttpServlet基类中service()方法的委派,请求会被转发到doGet()方法中。doGet()方法,在DispatcherServlet的父类FrameworkServlet类中被覆写。

1 @Override
2 protected final void doGet(HttpServletRequest
request, HttpServletResponse response)
3         throws ServletException,
IOException {
4  
5     processRequest(request,
response);
6 }

可以看到,这里只是简单的转发到processRequest()这个方法。

01 protected final void processRequest(HttpServletRequest
request, HttpServletResponse response)
02         throws ServletException,
IOException {
03  
04     long startTime
= System.currentTimeMillis();
05     Throwable
failureCause = 
null;
06  
07     //
Expose current LocaleResolver and request as LocaleContext.
08     LocaleContext
previousLocaleContext = LocaleContextHolder.getLocaleContext();
09     LocaleContextHolder.setLocaleContext(buildLocaleContext(request), this.threadContextInheritable);
10  
11     //
Expose current RequestAttributes to current thread.
12     RequestAttributes
previousRequestAttributes = RequestContextHolder.getRequestAttributes();
13     ServletRequestAttributes
requestAttributes = 
null;
14     if (previousRequestAttributes
== 
null ||
previousRequestAttributes.getClass().equals(ServletRequestAttributes.
class))
{
15         requestAttributes
new ServletRequestAttributes(request);
16         RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
17     }
18  
19     if (logger.isTraceEnabled())
{
20         logger.trace("Bound
request context to thread: "
 +
request);
21     }
22  
23     try {
24         doService(request,
response);
25     }
26     catch (ServletException
ex) {
27         failureCause
= ex;
28         throw ex;
29     }
30     catch (IOException
ex) {
31         failureCause
= ex;
32         throw ex;
33     }
34     catch (Throwable
ex) {
35         failureCause
= ex;
36         throw new NestedServletException("Request
processing failed"
,
ex);
37     }
38  
39     finally {
40         //
Clear request attributes and reset thread-bound context.
41         LocaleContextHolder.setLocaleContext(previousLocaleContext, this.threadContextInheritable);
42         if (requestAttributes
!= 
null)
{
43             RequestContextHolder.setRequestAttributes(previousRequestAttributes, this.threadContextInheritable);
44             requestAttributes.requestCompleted();
45         }
46         if (logger.isTraceEnabled())
{
47             logger.trace("Cleared
thread-bound request context: "
 +
request);
48         }
49  
50         if (logger.isDebugEnabled())
{
51             if (failureCause
!= 
null)
{
52                 this.logger.debug("Could
not complete request"
,
failureCause);
53             }
54             else {
55                 this.logger.debug("Successfully
completed request"
);
56             }
57         }
58         if (this.publishEvents)
{
59             //
Whether or not we succeeded, publish an event.
60             long processingTime
= System.currentTimeMillis() - startTime;
61             this.webApplicationContext.publishEvent(
62                     new ServletRequestHandledEvent(this,
63                             request.getRequestURI(),
request.getRemoteAddr(),
64                             request.getMethod(),
getServletConfig().getServletName(),
65                             WebUtils.getSessionId(request),
getUsernameForRequest(request),
66                             processingTime,
failureCause));
67         }
68     }
69 }

代码有点长,理解的要点是以doService()方法为区隔,前一部分是将当前请求的Locale对象和属性,分别设置到LocaleContextHolder和RequestContextHolder这两个抽象类中的ThreadLocal对象中,也就是分别将这两个东西和请求线程做了绑定。在doService()处理结束后,再恢复回请求前的LocaleContextHolder和RequestContextHolder,也即解除线程绑定。每次请求处理结束后,容器上下文都发布了一个ServletRequestHandledEvent事件,你可以注册监听器来监听该事件。

可以看到,processRequest()方法只是做了一些线程安全的隔离,真正的请求处理,发生在doService()方法中。点开FrameworkServlet类中的doService()方法。

1 protected abstract void doService(HttpServletRequest
request, HttpServletResponse response)
2         throws Exception;

又是一个抽象方法,这也是SpringMVC类设计中的惯用伎俩:父类抽象处理流程,子类给予具体的实现。真正的实现是在DispatcherServlet类中。

让我们接着看DispatcherServlet类中实现的doService()方法。

01 @Override
02 protected void doService(HttpServletRequest
request, HttpServletResponse response) 
throws Exception
{
03     if (logger.isDebugEnabled())
{
04         String
requestUri = urlPathHelper.getRequestUri(request);
05         logger.debug("DispatcherServlet
with name '"
 +
getServletName() + 
"'
processing "
 +
request.getMethod() +
06                 "
request for ["
 +
requestUri + 
"]");
07     }
08  
09     //
Keep a snapshot of the request attributes in case of an include,
10     //
to be able to restore the original attributes after the include.
11     Map<String,
Object> attributesSnapshot = 
null;
12     if (WebUtils.isIncludeRequest(request))
{
13         logger.debug("Taking
snapshot of request attributes before include"
);
14         attributesSnapshot
new HashMap<String,
Object>();
15         Enumeration<?>
attrNames = request.getAttributeNames();
16         while (attrNames.hasMoreElements())
{
17             String
attrName = (String) attrNames.nextElement();
18             if (this.cleanupAfterInclude
|| attrName.startsWith(
"org.springframework.web.servlet"))
{
19                 attributesSnapshot.put(attrName,
request.getAttribute(attrName));
20             }
21         }
22     }
23  
24     //
Make framework objects available to handlers and view objects.
25     request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE,
getWebApplicationContext());
26     request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
27     request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
28     request.setAttribute(THEME_SOURCE_ATTRIBUTE,
getThemeSource());
29  
30     FlashMap
inputFlashMap = 
this.flashMapManager.retrieveAndUpdate(request,
response);
31     if (inputFlashMap
!= 
null)
{
32         request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE,
Collections.unmodifiableMap(inputFlashMap));
33     }
34     request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
35     request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
36  
37     try {
38         doDispatch(request,
response);
39     }
40     finally {
41         //
Restore the original attribute snapshot, in case of an include.
42         if (attributesSnapshot
!= 
null)
{
43             restoreAttributesAfterInclude(request,
attributesSnapshot);
44         }
45     }
46 }

几个requet.setAttribute()方法的调用,将前面在初始化流程中实例化的对象设置到http请求的属性中,供下一步处理使用,其中有容器的上下文对象、本地化解析器等SpringMVC特有的编程元素。不同于Struts2中的ValueStack,SpringMVC的数据并没有从HttpServletRequest对象中抽离出来再存进另外一个编程元素,这也跟SpringMVC的设计思想有关。因为从一开始,SpringMVC的设计者就认为,不应该将请求处理过程和Web容器完全隔离。

所以,你可以看到,真正发生请求转发的方法doDispatch()中,它的参数是HttpServletRequest和HttpServletResponse对象。这给我们传递的意思也很明确,从request中能获取到一切请求的数据,从response中,我们又可以往服务器端输出任何响应,Http请求的处理,就应该围绕这两个对象来设计。我们不妨可以将SpringMVC这种设计方案,是从Struts2的过度设计中吸取教训,而向Servlet编程的一种回归和简化。

2.请求转发的抽象描述

接下来让我们看看doDispatch()这个整个请求转发流程中最核心的方法。DispatcherServlet所接收的Http请求,经过层层转发,最终都是汇总到这个方法中来进行最后的请求分发和处理。doDispatch()这个方法的内容,就是SpringMVC整个框架的精华所在。它通过高度抽象的接口,描述出了一个MVC(Model-View-Controller)设计模式的实现方案。Model、View、Controller三种层次的编程元素,在SpringMVC中都有大量的实现类,各种处理细节也是千差万别。但是,它们最后都是由,也都能由doDispatch()方法来统一描述,这就是接口和抽象的威力,万变不离其宗。

先来看一下doDispatch()方法的庐山真面目。

001 protected void doDispatch(HttpServletRequest
request, HttpServletResponse response) 
throws Exception
{
002     HttpServletRequest
processedRequest = request;
003     HandlerExecutionChain
mappedHandler = 
null;
004     int interceptorIndex
= -
1;
005  
006     try {
007         ModelAndView
mv;
008         boolean errorView
false;
009  
010         try {
011             processedRequest
= checkMultipart(request);
012  
013             //
Determine handler for the current request.
014             mappedHandler
= getHandler(processedRequest, 
false);
015             if (mappedHandler
== 
null ||
mappedHandler.getHandler() == 
null)
{
016                 noHandlerFound(processedRequest,
response);
017                 return;
018             }
019  
020             //
Determine handler adapter for the current request.
021             HandlerAdapter
ha = getHandlerAdapter(mappedHandler.getHandler());
022  
023             //
Process last-modified header, if supported by the handler.
024             String
method = request.getMethod();
025             boolean isGet
"GET".equals(method);
026             if (isGet
|| 
"HEAD".equals(method))
{
027                 long lastModified
= ha.getLastModified(request, mappedHandler.getHandler());
028                 if (logger.isDebugEnabled())
{
029                     String
requestUri = urlPathHelper.getRequestUri(request);
030                     logger.debug("Last-Modified
value for ["
 +
requestUri + 
"]
is: "
 +
lastModified);
031                 }
032                 if (new ServletWebRequest(request,
response).checkNotModified(lastModified) && isGet) {
033                     return;
034                 }
035             }
036  
037             //
Apply preHandle methods of registered interceptors.
038             HandlerInterceptor[]
interceptors = mappedHandler.getInterceptors();
039             if (interceptors
!= 
null)
{
040                 for (int i
0;
i < interceptors.length; i++) {
041                     HandlerInterceptor
interceptor = interceptors[i];
042                     if (!interceptor.preHandle(processedRequest,
response, mappedHandler.getHandler())) {
043                         triggerAfterCompletion(mappedHandler,
interceptorIndex, processedRequest, response, 
null);
044                         return;
045                     }
046                     interceptorIndex
= i;
047                 }
048             }
049  
050             //
Actually invoke the handler.
051             mv
= ha.handle(processedRequest, response, mappedHandler.getHandler());
052  
053             //
Do we need view name translation?
054             if (mv
!= 
null &&
!mv.hasView()) {
055                 mv.setViewName(getDefaultViewName(request));
056             }
057  
058             //
Apply postHandle methods of registered interceptors.
059             if (interceptors
!= 
null)
{
060                 for (int i
= interceptors.length - 
1;
i >= 
0;
i--) {
061                     HandlerInterceptor
interceptor = interceptors[i];
062                     interceptor.postHandle(processedRequest,
response, mappedHandler.getHandler(), mv);
063                 }
064             }
065         }
066         catch (ModelAndViewDefiningException
ex) {
067             logger.debug("ModelAndViewDefiningException
encountered"
,
ex);
068             mv
= ex.getModelAndView();
069         }
070         catch (Exception
ex) {
071             Object
handler = (mappedHandler != 
null ?
mappedHandler.getHandler() : 
null);
072             mv
= processHandlerException(processedRequest, response, handler, ex);
073             errorView
= (mv != 
null);
074         }
075  
076         //
Did the handler return a view to render?
077         if (mv
!= 
null &&
!mv.wasCleared()) {
078             render(mv,
processedRequest, response);
079             if (errorView)
{
080                 WebUtils.clearErrorRequestAttributes(request);
081             }
082         }
083         else {
084             if (logger.isDebugEnabled())
{
085                 logger.debug("Null
ModelAndView returned to DispatcherServlet with name '"
 +
getServletName() +
086                         "':
assuming HandlerAdapter completed request handling"
);
087             }
088         }
089  
090         //
Trigger after-completion for successful outcome.
091         triggerAfterCompletion(mappedHandler,
interceptorIndex, processedRequest, response, 
null);
092     }
093  
094     catch (Exception
ex) {
095         //
Trigger after-completion for thrown exception.
096         triggerAfterCompletion(mappedHandler,
interceptorIndex, processedRequest, response, ex);
097         throw ex;
098     }
099     catch (Error
err) {
100         ServletException
ex = 
new NestedServletException("Handler
processing failed"
,
err);
101         //
Trigger after-completion for thrown exception.
102         triggerAfterCompletion(mappedHandler,
interceptorIndex, processedRequest, response, ex);
103         throw ex;
104     }
105  
106     finally {
107         //
Clean up any resources used by a multipart request.
108         if (processedRequest
!= request) {
109             cleanupMultipart(processedRequest);
110         }
111     }
112 }

真是千呼万唤始出来,犹抱琵琶半遮面。我们在第一篇《SpringMVC源码剖析(一)- 从抽象和接口说起》中所描述的各种编程元素,依次出现在该方法中。HandlerMapping、HandlerAdapter、View这些接口的设计,我们在第一篇中已经讲过。现在我们来重点关注一下HandlerExecutionChain这个对象。

从上面的代码中,很明显可以看出一条线索,整个方法是围绕着如何获取HandlerExecutionChain对象,执行HandlerExecutionChain对象得到相应的视图对象,再对视图进行渲染这条主线来展开的。HandlerExecutionChain对象显得异常重要。

因为Http请求要进入SpringMVC的处理体系,必须由HandlerMapping接口的实现类映射Http请求,得到一个封装后的HandlerExecutionChain对象。再由HandlerAdapter接口的实现类来处理这个HandlerExecutionChain对象所包装的处理对象,来得到最后渲染的视图对象。

视图对象是用ModelAndView对象来描述的,名字已经非常直白,就是数据和视图,其中的数据,由HttpServletRequest的属性得到,视图就是由HandlerExecutionChain封装的处理对象处理后得到。当然HandlerExecutionChain中的拦截器列表HandlerInterceptor,会在处理过程的前后依次被调用,为处理过程留下充足的扩展点。

所有的SpringMVC框架元素,都是围绕着HandlerExecutionChain这个执行链来发挥效用。我们来看看,HandlerExecutionChain类的代码。

01 package org.springframework.web.servlet;
02  
03 import java.util.ArrayList;
04 import java.util.Arrays;
05 import java.util.List;
06  
07 import org.springframework.util.CollectionUtils;
08  
09 public class HandlerExecutionChain
{
10  
11     private final Object
handler;
12  
13     private HandlerInterceptor[]
interceptors;
14  
15     private List<HandlerInterceptor>
interceptorList;
16  
17     public HandlerExecutionChain(Object
handler) {
18         this(handler, null);
19     }
20  
21     public HandlerExecutionChain(Object
handler, HandlerInterceptor[] interceptors) {
22         if (handler instanceof HandlerExecutionChain)
{
23             HandlerExecutionChain
originalChain = (HandlerExecutionChain) handler;
24             this.handler
= originalChain.getHandler();
25             this.interceptorList
new ArrayList<HandlerInterceptor>();
26             CollectionUtils.mergeArrayIntoCollection(originalChain.getInterceptors(), this.interceptorList);
27             CollectionUtils.mergeArrayIntoCollection(interceptors, this.interceptorList);
28         }
29         else {
30             this.handler
= handler;
31             this.interceptors
= interceptors;
32         }
33     }
34  
35     public Object
getHandler() {
36         return this.handler;
37     }
38  
39     public void addInterceptor(HandlerInterceptor
interceptor) {
40         initInterceptorList();
41         this.interceptorList.add(interceptor);
42     }
43  
44     public void addInterceptors(HandlerInterceptor[]
interceptors) {
45         if (interceptors
!= 
null)
{
46             initInterceptorList();
47             this.interceptorList.addAll(Arrays.asList(interceptors));
48         }
49     }
50  
51     private void initInterceptorList()
{
52         if (this.interceptorList
== 
null)
{
53             this.interceptorList
new ArrayList<HandlerInterceptor>();
54         }
55         if (this.interceptors
!= 
null)
{
56             this.interceptorList.addAll(Arrays.asList(this.interceptors));
57             this.interceptors
null;
58         }
59     }
60  
61     public HandlerInterceptor[]
getInterceptors() {
62         if (this.interceptors
== 
null && this.interceptorList
!= 
null)
{
63             this.interceptors
this.interceptorList.toArray(new HandlerInterceptor[this.interceptorList.size()]);
64         }
65         return this.interceptors;
66     }
67  
68     @Override
69     public String
toString() {
70         if (this.handler
== 
null)
{
71             return "HandlerExecutionChain
with no handler"
;
72         }
73         StringBuilder
sb = 
new StringBuilder();
74         sb.append("HandlerExecutionChain
with handler ["
).append(this.handler).append("]");
75         if (!CollectionUtils.isEmpty(this.interceptorList))
{
76             sb.append("
and "
).append(this.interceptorList.size()).append("
interceptor"
);
77             if (this.interceptorList.size()
1)
{
78                 sb.append("s");
79             }
80         }
81         return sb.toString();
82     }
83  
84 }

一个拦截器列表,一个执行对象,这个类的内容十分的简单,它蕴含的设计思想,却十分的丰富。

1.拦截器组成的列表,在执行对象被调用的前后,会依次执行。这里可以看成是一个的AOP环绕通知,拦截器可以对处理对象随心所欲的进行处理和增强。这里明显是吸收了Struts2中拦截器的设计思想。这种AOP环绕式的扩展点设计,也几乎成为所有框架必备的内容。

2.实际的处理对象,即handler对象,是由Object对象来引用的。

1 private final Object
handler;

之所以要用一个java世界最基础的Object对象引用来引用这个handler对象,是因为连特定的接口也不希望绑定在这个handler对象上,从而使handler对象具有最大程度的选择性和灵活性。

我们常说,一个框架最高层次的抽象是接口,但是这里SpringMVC更进了一步。在最后的处理对象上面,SpringMVC没有对它做任何的限制,只要是java世界中的对象,都可以用来作为最后的处理对象,来生成视图。极端一点来说,你甚至可以将另外一个MVC框架集成到SpringMVC中来,也就是为什么SpringMVC官方文档中,居然还有集成其他表现层框架的内容。这一点,在所有表现层框架中,是独领风骚,冠绝群雄的。

3.结语

SpringMVC的成功,源于它对开闭原则的运用和遵守。也正因此,才使得整个框架具有如此强大的描述和扩展能力。这也许和SpringMVC出现和兴起的时间有关,正是经历了Struts1到Struts2这些Web开发领域MVC框架的更新换代,它的设计者才能站在前人的肩膀上。知道了如何将事情做的糟糕之后,你或许才知道如何将事情做得好。

希望在这个系列里面分享的SpringMVC源码阅读经验,能帮助读者们从更高的层次来审视SpringMVC框架的设计,也希望这里所描述的一些基本设计思想,能在你更深入的了解SpringMVC的细节时,对你有帮助。哲学才是唯一的、最终的武器,在一个框架的设计上,尤其是如此。经常地体会一个框架设计者的设计思想,对你更好的使用它,是有莫大的益处的。

SpringMVC源码剖析(四)- DispatcherServlet请求转发的的更多相关文章

  1. SpringMVC源码剖析(四)- DispatcherServlet请求转发的实现

    SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段.在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过serv ...

  2. SpringMVC源码剖析(二)- DispatcherServlet的前世今生

    上一篇文章<SpringMVC源码剖析(一)- 从抽象和接口说起>中,我介绍了一次典型的SpringMVC请求处理过程中,相继粉墨登场的各种核心类和接口.我刻意忽略了源码中的处理细节,只列 ...

  3. SpringMVC源码剖析1——执行流程

    SpringMVC源码剖析1——执行流程 00.SpringMVC执行流程file:///C:/Users/WANGGA~1/AppData/Local/Temp/enhtmlclip/Image.p ...

  4. SpringMVC源码情操陶冶-DispatcherServlet

    本文对springmvc核心类DispatcherServlet作下简单的向导,方便博主与读者查阅 DispatcherServlet-继承关系 分析DispatcherServlet的继承关系以及主 ...

  5. SpringMVC源码情操陶冶-DispatcherServlet父类简析

    阅读源码有助于陶冶情操,本文对springmvc作个简单的向导 springmvc-web.xml配置 <servlet> <servlet-name>dispatch< ...

  6. SpringMVC源码情操陶冶-DispatcherServlet类简析(一)

    阅读源码有利于陶冶情操,此文承接前文SpringMVC源码情操陶冶-DispatcherServlet父类简析 注意:springmvc初始化其他内容,其对应的配置文件已被加载至beanFactory ...

  7. SpringMVC源码情操陶冶-DispatcherServlet简析(二)

    承接前文SpringMVC源码情操陶冶-DispatcherServlet类简析(一),主要讲述初始化的操作,本文将简单介绍springmvc如何处理请求 DispatcherServlet#doDi ...

  8. Django Rest Framework源码剖析(四)-----API版本

    一.简介 在我们给外部提供的API中,可会存在多个版本,不同的版本可能对应的功能不同,所以这时候版本使用就显得尤为重要,django rest framework也为我们提供了多种版本使用方法. 二. ...

  9. SpringMVC源码剖析(五)-消息转换器HttpMessageConverter

    原文链接:https://my.oschina.net/lichhao/blog/172562 #概述 在SpringMVC中,可以使用@RequestBody和@ResponseBody两个注解,分 ...

  10. SpringMVC源码剖析5:消息转换器HttpMessageConverter与@ResponseBody注解

    转自 SpringMVC关于json.xml自动转换的原理研究[附带源码分析] 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Spring源码 ...

随机推荐

  1. springboot 静态文件夹

    正常这个很久了,不需要写,但是好几年没有写这个相关的,都忘了,好记性不如烂笔头 spring: resources: static-locations: file:D:\\test #对应服务器内映射 ...

  2. 在虚拟机CentOS中安装jdk

    公众号本文地址:在虚拟机CentOS中安装jdk 本文主要是记录在CentOS中安装新的JDK的过程. 在虚拟机的centos中安装Jdk主要分为三步,第一步上传jdk文件到centos中,第二步解压 ...

  3. vue打包后dist的使用

    发现问题 vue项目完成打包出dist后准备打开index.html,发现居然页面是一片空白,f12一片报红. 分析问题 经过多次网上查询后发现这是由于vue打包时,脚手架会帮你配置好大量参数,但其中 ...

  4. git 批量删除本地分支及远程分支

    git 批量删除本地分支及远程分支 一.批量删除本地分支 git branch |grep 'name' |xargs git branch -D 备注: name 为需要匹配的分支名称 二.批量删除 ...

  5. 痞子衡嵌入式:在MDK开发环境下自定义安装与切换不同编译器版本的方法

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是在MDK开发环境下自定义安装与切换不同编译器版本的方法. Keil MDK 想必是嵌入式开发者最熟悉的工具之一了,自 2005 年 Ar ...

  6. JAVA基础之5-函数式接口的实现

    之所以单独把这个列出来,是因为本人被一个源码给震撼了. 所以,本人目的是看看这个震撼实现,并模仿,最后把常规的实现也贴上,让读者可以看到相对完整的实现 注:本文代码基于JDK17 一.让人震撼的代码 ...

  7. 基于DPAPI+RDP技术实现本地打开远程程序,并映射到本地机器桌面上

    本教程使用工具所使用的环境说明: 启动器开发工具:VS2022 启动器所用客户端技术:.NET 8 + WPF 启动器其他技术:DPAPI 启动器发布的可执行程序,系统要求:Windows 7以及以上 ...

  8. 【题目全解】ACGO排位赛#13

    ACGO排位赛#13 - 题目解析 感谢大家参加本次排位赛! T1 - 纪元流星雨 题目链接跳转:点击跳转 也没有特别大的难度,手动模拟一下就可以了. 解题步骤 先计算出这个人一生中第一次看到流星雨的 ...

  9. 2020年度国产数据库:openGauss

    根据墨天轮2020年一年的数据库流行度得分趋势变化,我们选出了流行热度增长显著的数据库为2020年度国产数据库.恭喜 华为开源关系型数据库 openGauss 荣获 "2020年度国产数据库 ...

  10. 2021年11月墨天轮国产数据库排行榜:openGauss闯入前三,Kingbase流行度与日俱增,TDengine厚积薄发

    2021年11月的国产数据库流行度排行榜已在墨天轮发布,本月共有163家数据库参与排名.就前15名的总体情况来看,除openGauss反超OceanBase闯入前三,TDengine厚积薄发来到第15 ...