• spring mvc


    MVC

    Model(模型):数据模型,包含数据和行为:Value Object(数据) 和 服务层(行为)。

    View(视图):负责进行模型的展示。

    Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。 也就是说控制器做了个调度员的工作。

    模型无法主动推数据给视图,如果用户想要视图更新,需要发送一次请求(即请求-响应模型)。

    Web端开发发展历程

    CGI(Common Gateway Interface):在web服务端使用的脚本技术,用于接收web用户请求并处理,最后动态产生响应给用户,但每次请求将产生一个进程,重量级

    Servlet:JavaEE web组件技术,一种在服务器端执行的web组件,用于接收web用户请求并处理,最后动态产生响应给用户。每次请求只产生一个线程(而且有线程池),轻量级。在java代码里面输出html流,表现逻辑、控制逻辑、业务逻辑调用混杂

    JSP(Java Server Page):一种在服务器端执行的web组件,嵌入脚本语言的模板页面技术。在html代码中嵌入java代码。JSP最终会被编译为Servlet,比纯Servlet开发页面简单、方便,但表现逻辑、控制逻辑、业务逻辑调用还是混杂

    Model1:JSP+JavaBean,使用<jsp:useBean>标准动作,将请求参数封装为JavaBean组件,还须使用java脚本执行控制逻辑。

    Model2:控制器采用Servlet、模型采用JavaBean、视图采用JSP。只是一种代码上物理的分离,实则依赖关系很严重。

    服务到工作者:Front Controller + Application Controller + Page Controller + Context

    即,前端控制器+应用控制器+页面控制器(也有称其为动作)+上下文,也是Web MVC,只是责任更加明确。

    Spring Web MVC

    基于Java,实现了Web MVC设计模式,请求驱动类型的轻量级Web框架(基于请求驱动:使用请求-响应模型)

    前端控制器是DispatcherServlet

    应用控制器拆为处理器映射器Handler Mapping进行处理器管理和视图解析器View Resolver进行视图管理;

    页面控制器/动作为Controller接口(仅包含ModelAndView handleRequest(request, response) 方法)的实现(也可以是任何的POJO类);

    Spring MVC执行流程

    1. 用户请求DispatcherServlet。
    2. DispatcherServlet接受到请求,将根据请求信息交给处理器映射器。
    3. 处理器映射器(HandlerMapping)根据请求路径查找匹配的Handler,并返回一个执行链。
    4. DispatcherServlet再根据执行链请求处理器适配器(HandlerAdapter)。
    5. 处理器适配器调用相应的handler进行功能处理。
    6. 对应的handler处理完成后返回ModelAndView给处理器适配器。
    7. 处理器适配器将接受的ModelAndView返回给DispatcherServlet。
    8. DispatcherServlet请求视图解析器来解析视图。
    9. 视图解析器处理完后返回View对象给DispatcherServlet。
    10. 最后前端控制器对View进行视图渲染(即将模型数据填充至视图中)。
    11. DispatcherServlet返回响应给用户

    DispatcherServlet:拦截请求到Spring Web MVC

    HandlerMapping:将请求映射到处理器

    HandlerAdapter:支持多种类型的处理器

    Handler:如Controller,进行功能处理

    ViewResolver:将逻辑视图名解析为具体视图技术

    View:视图

    DispatcherServlet配置

    1、基于xml配置
    <servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-servlet-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVCDemo</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    

    其中<load-on-startup>1</load-on-startup>标志容器是否在启动时就加载该servlet。值表示被载入的顺序:>=0表示容器启动时就加载,否则被请求才加载;值越小,优先级越高。

    2、基于Spring Boot配置
    @SpringBootApplication
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    @SpringBootApplication这个注解等同于:

    @Configuration	// @SpringBootConfiguration就是用的这个
    @EnableAutoConfiguration
    @ComponentScan
    

    关注@EnableAutoConfiguration注解,它有一个@Import({AutoConfigurationImportSelector.class})注解。借助AutoConfigurationImportSelector@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。这里也是DispatcherServlet初始化的关键。

    // AutoConfigurationImportSelector
    	@Override
    	// 该方法返回的String数组是全类名,会被纳入容器中。
    	// DispatcherServlet的全类名就在这个数组中
    	public String[] selectImports(AnnotationMetadata annotationMetadata) {
    		// getAutoConfigurationEntry-->getCandidateConfigurations
    		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    	}
    
        protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
            // 把spring-boot-autoconfigure.jar的spring.factories中
            // 以EnableAutoConfiguration为key的value类加载到容器中
            // this.getSpringFactoriesLoaderFactoryClass()就是EnableAutoConfiguration.class
            List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
            ...
            return configurations;
        }
    

    org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration就在其中,于是DispatcherServletAutoConfiguration被载入。

    // SpringFactoriesLoader
    	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    
    	public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
            // EnableAutoConfiguration
    		String factoryClassName = factoryClass.getName();
    		return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
    	}
    
    	private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
            // 先看一下缓存里有没有这个map,有就直接返回了。
    		...
    		try {
                // 从spring.factories文件获取资源
    			Enumeration<URL> urls = (classLoader != null ?
    					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
    					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    			result = new LinkedMultiValueMap<>();
    			while (urls.hasMoreElements()) {
    				URL url = urls.nextElement();
    				UrlResource resource = new UrlResource(url);
    				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
    				for (Map.Entry<?, ?> entry : properties.entrySet()) {
    					String factoryClassName = ((String) entry.getKey()).trim();
    					for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                            // 以键值对形式存储
    						result.add(factoryClassName, factoryName.trim());
    					}
    				}
    			}
                // 放到缓存map中
    			cache.put(classLoader, result);
    			return result;
    		}...
    	}
    

    控制器定义

    @Controller注解表明了一个类是作为控制器的角色而存在的。Spring不要求你去继承任何控制器基类,也不要求你去实现Servlet的那套API。

    // 用于标识是处理器类
    @Controller
    public class GreetingController {
        // @GetMapping确保到/greeting的HTTP GET请求被映射到当前这个方法上
        @GetMapping("/greeting")
        // @RequestParam将请求参数绑定到方法参数上:required=false该查询参数非必须;defaultValue="World"如果没有传入该值,还有默认值
        public String greeting(@RequestParam(name="name", required=false, defaultValue="World") String name, Model model) {
            // name参数的值被加到Model对象上, 最终在视图上展现
            model.addAttribute("name", name);
            return "greeting";
        }
    }
    

    分派器(DispatcherServlet)会扫描所有注解了@Controller的类,检测其中通过@RequestMapping注解配置的方法。(RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter)

    DispatcherServlet初始化

    1、初始化参数

    基于xml

    DispatcherServlet的初始化过程中,Spring MVC会在web应用的WEB-INF目录下查找一个名为[servlet-name]-servlet.xml的配置文件,并创建其中所定义的bean。(就是IoC容器创建bean的流程!)

    如果使用如上配置,Spring Web MVC框架将加载“classpath:spring-servlet-config.xml”来进行初始化上下文而不是“/WEB-INF/[servlet名字]-servlet.xml”。

    基于Spring Boot

    Configuring the DispatcherServlet yourself is unusual but if you really need to do it, a @Bean of type DispatcherServletPath must be provided as well to provide the path of your custom DispatcherServlet.

    2、初始化流程

    HttpServletBean继承HttpServlet,因此在Web容器启动时将调用它的init方法:

    	// 这个方法会在容器初始化每个Servlet的时候被调用一次。方法是在GenericServlet中有一个空定义
    	public final void init() throws ServletException {
            // 将Servlet初始化参数设置到该组件上
            PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
            if (!pvs.isEmpty()) {
                try {
                    // 通过BeanWrapper简化设值过程,方便后续使用
                    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                    ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
                    bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
                    this.initBeanWrapper(bw);
                    bw.setPropertyValues(pvs, true);
                }...
            }
    		// 提供给子类初始化扩展点,该方法由FrameworkServlet覆盖。
            this.initServletBean();
        }
    
    

    FrameworkServlet继承HttpServletBean

        // 进行Web上下文初始化
    	protected final void initServletBean() throws ServletException {
            ....
            long startTime = System.currentTimeMillis();
            try {
                // 初始化web上下文
                this.webApplicationContext = this.initWebApplicationContext();
                this.initFrameworkServlet();
            }
            ...
        }
        
        protected WebApplicationContext initWebApplicationContext() {
            // ROOT上下文
            // this.getServletContext():
            // GenericServlet(this.getServletConfig().getServletContext();)
            // getWebApplicationContext():
            // getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)
            // 就是这里得到了DispatcherServlet的上下文!
            WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
            WebApplicationContext wac = null;
            if (this.webApplicationContext != null) {
                // 1、在创建该Servlet时注入的上下文
                wac = this.webApplicationContext;
                if (wac instanceof ConfigurableWebApplicationContext) {
                    ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
                    if (!cwac.isActive()) {
                        if (cwac.getParent() == null) {
                            cwac.setParent(rootContext);
                        }
    
                        this.configureAndRefreshWebApplicationContext(cwac);
                    }
                }
            }
    		// 2、查找已经绑定的上下文
            if (wac == null) {
                wac = this.findWebApplicationContext();
            }
    		// 3、如果没有找到相应的上下文,创建并指定父亲为根上下文
            if (wac == null) {
                wac = this.createWebApplicationContext(rootContext);
            }
    
            if (!this.refreshEventReceived) {
                synchronized(this.onRefreshMonitor) {
                    // 4、刷新上下文(执行一些初始化)
                	// 提供给子类初始化扩展点
                    this.onRefresh(wac);
                }
            }
    
            if (this.publishContext) {
                String attrName = this.getServletContextAttributeName();
                this.getServletContext().setAttribute(attrName, wac);
            }
    
            return wac;
        }
    
    

    DispatcherServlet继承FrameworkServlet

        protected void onRefresh(ApplicationContext context) {
            this.initStrategies(context);
        }
    
        // 初始化默认的Spring Web MVC框架使用的策略
    	protected void initStrategies(ApplicationContext context) {
            this.initMultipartResolver(context);
            this.initLocaleResolver(context);
            this.initThemeResolver(context);
            this.initHandlerMappings(context);
            this.initHandlerAdapters(context);
            this.initHandlerExceptionResolvers(context);
            this.initRequestToViewNameTranslator(context);
            this.initViewResolvers(context);
            this.initFlashMapManager(context);
        }
    

    总结

    整个DispatcherServlet初始化的过程具体主要做了如下两件事情:

    1、初始化Spring Web MVC使用的Web上下文,并且可能指定父容器为根上下文;

    2、初始化DispatcherServlet使用的策略,如HandlerMapping、HandlerAdapter等。

    HandlerMapping初始化

    SimpleUrlHandlerMapping是Spring MVC中适用性最强的HandlerMapping类,允许明确指定URL模式和Handler的映射关系。

        private void initHandlerMappings(ApplicationContext context) {
            this.handlerMappings = null;
            // 优先判断detectAllHandlerMappings的值。
            if (this.detectAllHandlerMappings) {
                // 默认情况下,Spring MVC会加载在当前系统中所有实现了HandlerMapping接口的bean
                // 再进行按优先级排序。(优先级通过Ordered接口设定)
                // Find all HandlerMappings in the ApplicationContext, 
                // including ancestor contexts.
                Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
                if (!matchingBeans.isEmpty()) {
                    this.handlerMappings = new ArrayList(matchingBeans.values());
                    // We keep HandlerMappings in sorted order.
                    AnnotationAwareOrderComparator.sort(this.handlerMappings);
                }
            } else {
                try {
                    // detectAllHandlerMappings值设置为false
                    // Spring MVC就只会查找名为“handlerMapping”的bean
                    // 并作为当前系统的唯一的HandlerMapping
                    HandlerMapping hm = (HandlerMapping)context.getBean("handlerMapping", HandlerMapping.class);
                    this.handlerMappings = Collections.singletonList(hm);
                } catch (NoSuchBeanDefinitionException var3) {
                }
            }
            // Ensure we have at least one HandlerMapping, by registering
    		// a default HandlerMapping if no other mappings are found.
            if (this.handlerMappings == null) {
                // 没有定义HandlerMapping的话
                // 按照DispatcherServlet.properties所定义的内容来加载默认的HandlerMapping。
                this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class);
                ...
            }
    
        }
    

    其中,defaultStrategies从静态代码段中获得:

    ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
    defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    

    HandlerAdapter初始化

    HandlerAdapter可以帮助自定义各种Handler。具体实现同上。

    明显是适配器模式。也就是说被适配者是handler。而适配器都实现了HandlerAdapter接口。

        private void initHandlerAdapters(ApplicationContext context) {
            this.handlerAdapters = null;
            if (this.detectAllHandlerAdapters) {
                Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
                if (!matchingBeans.isEmpty()) {
                    this.handlerAdapters = new ArrayList(matchingBeans.values());
                    AnnotationAwareOrderComparator.sort(this.handlerAdapters);
                }
            } else {
                try {
                    HandlerAdapter ha = (HandlerAdapter)context.getBean("handlerAdapter", HandlerAdapter.class);
                    this.handlerAdapters = Collections.singletonList(ha);
                } catch (NoSuchBeanDefinitionException var3) {
                }
            }
    
            if (this.handlerAdapters == null) {
                this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class);
                ...
            }
    
        }
    
    抽象处理器方法适配器(AbstractHandlerMethodAdapter)
    1. afterPropertiesSet方法注入ArgumentResolversReturnValueHandlers到Spring容器。

    2. ServletInvocableHandlerMethod调用invokeAndHandle方法。

    3. 两大接口:HandlerMethodArgumentResolver、HandlerMethodReturnValueHandler。

      使用HandlerMethodReturnValueComposite,使用组合模式,放入HandlerMethodReturnValueHandler的list;同理,HandlerMethodArgumentResolverComposite使用组合模式,放入HandlerMethodArgumentResolver的list。

    4. RequestResponseBodyMethodProcessor负责解析Controller里@RequestBody,支持响应类型是@ResponseBody

      RequestParamMethodArgumentResolver负责解析Controller里@RequestParam

      这两个都是resolver!都实现了两大接口!

    // RequestMappingHandlerAdapter
    	@Nullable
        protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
            ...
            try {
                ...
                ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);
                if (this.argumentResolvers != null) {
                    invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
                }
    
                if (this.returnValueHandlers != null) {
                    invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
                }
                ...
                // ★
                invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
                ...
            }...
        }
    
    // ServletInvocableHandlerMethod
    	public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
            // ★方法参数处理,见下
            Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
            this.setResponseStatus(webRequest);
            if (returnValue == null) {
                if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {
                    this.disableContentCachingIfNecessary(webRequest);
                    mavContainer.setRequestHandled(true);
                    return;
                }
            } else if (StringUtils.hasText(this.getResponseStatusReason())) {
                mavContainer.setRequestHandled(true);
                return;
            }
            ...
            try {
                // 返回值的处理 ★ 里面是用的组合,不同子类实现不同
                this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
            }...
        }
    
    // InvocableHandlerMethod
    	@Nullable
        public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
            // 解析请求参数
            Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
            ...
            // 调用Controller中的请求方法
            return this.doInvoke(args);
        }
    
    HTTP请求处理器适配器(HttpRequestHandlerAdapter)

    仅支持对HTTP请求处理器的适配。

    Handler需实现HttpRequestHandler接口,并实现其void handlerRequest(HttpRequest,HttpResponse)方法。

    简单控制器处理器适配器(SimpleControllerHandlerAdapter)

    将HTTP请求适配到一个控制器的实现进行处理。这里的控制器的实现是一个简单的控制器接口的实现。客户化的业务逻辑通常是在控制器接口的实现类中实现的。

    Handler需实现Controller接口,并实现其ModelAndView handler(HttpRequest,HttpResponse,handler)方法。

    ViewResolver初始化

    具体实现同上。

        private void initViewResolvers(ApplicationContext context) {
            this.viewResolvers = null;
            if (this.detectAllViewResolvers) {
                Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
                if (!matchingBeans.isEmpty()) {
                    this.viewResolvers = new ArrayList(matchingBeans.values());
                    AnnotationAwareOrderComparator.sort(this.viewResolvers);
                }
            } else {
                try {
                    ViewResolver vr = (ViewResolver)context.getBean("viewResolver", ViewResolver.class);
                    this.viewResolvers = Collections.singletonList(vr);
                } catch (NoSuchBeanDefinitionException var3) {
                }
            }
    
            if (this.viewResolvers == null) {
                this.viewResolvers = this.getDefaultStrategies(context, ViewResolver.class);
                ...
            }
    
        }
    

    DispatcherServlet分派

    // DispatcherServlet
    	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
            HttpServletRequest processedRequest = request;
            HandlerExecutionChain mappedHandler = null;
            boolean multipartRequestParsed = false;
            WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    
            try {
                try {
                    ModelAndView mv = null;
                    Object dispatchException = null;
    
                    try {
                         // 检查请求是否是multipart(如文件上传),如果是将通过MultipartResolver解析 
                        processedRequest = this.checkMultipart(request);
                        multipartRequestParsed = processedRequest != request;
                        // 2、请求到处理器(页面控制器)的映射,通过HandlerMapping进行映射
                        // HandlerMapping将会把请求映射为HandlerExecutionChain对象
                        // 包含一个Handler处理器、多个HandlerInterceptor拦截器
                        mappedHandler = this.getHandler(processedRequest);
                        if (mappedHandler == null) {
                            this.noHandlerFound(processedRequest, response);
                            return;
                        }
                        // 3、处理器适配,即将处理器包装成相应的适配器(从而支持多种类型的处理器)
                        // 会轮询适配器模,查找能够处理当前请求的处理器的实现
                        HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                        
                        // 304 Not Modified缓存支持
                        ...
                        // 执行处理器相关的拦截器的预处理(HandlerInterceptor.preHandle)
                        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                            return;
                        }
                        // 4、由适配器执行处理器(调用处理器相应功能处理方法),返回一个ModelAndView对象
                        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                        if (asyncManager.isConcurrentHandlingStarted()) {
                            return;
                        }
    
                        this.applyDefaultViewName(processedRequest, mv);
                        // 执行处理器相关的拦截器的后处理(HandlerInterceptor.postHandle)
                        mappedHandler.applyPostHandle(processedRequest, response, mv);
                    }...
                    // 5&6、解析视图并进行视图的渲染  
                    this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
                }...
            } finally {
                if (asyncManager.isConcurrentHandlingStarted()) {
                    if (mappedHandler != null) {
                        mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                    }
                } else if (multipartRequestParsed) {
                    // Clean up any resources used by a multipart request.  
                    this.cleanupMultipart(processedRequest);
                }
    
            }
        }
    
        private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
            boolean errorView = false;
            if (exception != null) {
                ...
            }
    		// 5、由ViewResolver解析View
            // viewName不为空:viewResolver.resolveViewName(viewName, locale)
            // viewName为空:mv.getView()
            // 6、视图在渲染时会把Model传入
            // view.render(mv.getModelInternal(), request, response);这是个接口方法
            // 接口名为View:具体实现有StaticView、ThymeleafView、HtmlResourceView和AbstractView等
            if (mv != null && !mv.wasCleared()) {
                // ViewResolver将把逻辑视图名解析为具体的View(很容易更换其他视图技术)
                // View会根据传进来的Model模型数据进行渲染
                this.render(mv, request, response);
                if (errorView) {
                    WebUtils.clearErrorRequestAttributes(request);
                }
            }....
    
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                if (mappedHandler != null) {
                    mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
                }
    
            }
        }
    
    
  • 相关阅读:
    用户态切换到内核态的3种方式
    vim_action
    import date
    __sizeof__()
    classmethod staticmethod
    Java对对象的引用 不是 引用调用 而是按值引用 Java不存在引用调用
    多线程同步
    Does Hadoop require SSH?
    hdfs namenode出错
    软件项目的一致性语义描述
  • 原文地址:https://www.cnblogs.com/angelica-duhurica/p/11248607.html
Copyright © 2020-2023  润新知