废话

最近在看SpringMVC...里面东西好多...反正东看一点西看一点吧...

分享一下最近的一些心得..是关于DispatcherServlet的

DispatcherServlet与ContextLoaderListener

dispattcherServlet这个类大家肯定不陌生的...因为使用SpringMVC一定会在web.xml里配置它.

     <servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!-- 默认/WEB-INF/[servlet名字]-servlet.xml加载上下文, 如果配置了contextConfigLocation参数,
将使用classpath:/mvc-dispatcher-servlet.xml加载上下文 -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/spring/mvc-dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

从配置中就可以看出这是1个servlet.

而使用SpringMVC需要使用Spring,Spring在web环境中时通过一个listener来配置的

     <listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener> <!-- 配置spring的bean用 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:/spring/application-context.xml
</param-value>
</context-param>
 public class ContextLoaderListener extends ContextLoader implements ServletContextListener {.....}

ContextLoaderListener实现了ServletContextListener.

 public interface ServletContextListener extends EventListener {
/**
** Notification that the web application initialization
** process is starting.
** All ServletContextListeners are notified of context
** initialization before any filter or servlet in the web
** application is initialized.
*/ public void contextInitialized ( ServletContextEvent sce ); /**
** Notification that the servlet context is about to be shut down.
** All servlets and filters have been destroy()ed before any
** ServletContextListeners are notified of context
** destruction.
*/
public void contextDestroyed ( ServletContextEvent sce );
}

ServletContextListener的contextInitialized是在任何filter或者servlet之前调用的.

贴了这么多代码片段只为了说明一个问题:Spring的applicationContext先于SpringMVC的applicationContext加载.

HttpServletBean

DispatcherServlet继承自FrameworkServlet继承自HttpServletBean继承自HttpServlet

所以我觉得按时间顺序的话应该从HttpServletBean的init方法看起.

 @Override
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
} // Set bean properties from init parameters.
try {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
throw ex;
} // Let subclasses do whatever initialization they like.
initServletBean(); if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}

9-14行主要就是把当前类的对象(dispatcherServlet)包装成BeanWrapperImpl对象,然后设置属性.

PropertyValues可以来自ServletConfig,然后PropertyValues又会被设置到dispatcher的属性里去.

从中我们可以得到的信息是什么呢?

我们可以得到的信息就是:

         <init-param>
<!-- 默认/WEB-INF/[servlet名字]-servlet.xml加载上下文, 如果配置了contextConfigLocation参数,
将使用classpath:/mvc-dispatcher-servlet.xml加载上下文 -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/spring/mvc-dispatcher-servlet.xml</param-value>
</init-param>

dispatcherServlet里面的init-param我们可以配置dispatcherServlet里的所有属性名称和值

    /** ServletContext attribute to find the WebApplicationContext in */
private String contextAttribute; /** WebApplicationContext implementation class to create */
private Class<?> contextClass = DEFAULT_CONTEXT_CLASS; /** WebApplicationContext id to assign */
private String contextId; /** Namespace for this servlet */
private String namespace; /** Explicit context config location */
private String contextConfigLocation; ...............

做完这些以后就转到22行,让FrameworkServlet来做剩下的事情

FrameworkServlet

 /**
* Overridden method of {@link HttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis(); try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
} if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}

FrameworkServlet我觉得就做了一件事情.就是初始化了applicationContext(第14行).只是这个初始化非常复杂.....

然后留了1个扩展点:

第15行initFrameworkServlet()现在的实现是空的.它会在applicationContext初始化完成之后被调用.

所以我们可以通过继承dispatcherServlet来扩展.但是话又说回来.Spring的扩展点真的很多.可以通过继承很多接口来参与Spring bean的生命周期.也并不一定要通过这个方法来做.

然后再来看看initWebApplicationContext方法...这个方法里面嵌套了N层..

 /**
* Initialize and publish the WebApplicationContext for this servlet.
* <p>Delegates to {@link #createWebApplicationContext} for actual creation
* of the context. Can be overridden in subclasses.
* @return the WebApplicationContext instance
* @see #FrameworkServlet(WebApplicationContext)
* @see #setContextClass
* @see #setContextConfigLocation
*/
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null; if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
} if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
onRefresh(wac);
} if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
} return wac;
}

里面有很多种情况,很多if情况我都没遇到过..我只知道在我最一般的配置下.这个时候Spring的rootContext(11行)是已经完成了初始化.而SpringMVC的wac applicationContext还没有初始化.所以这个时候wac = createWebApplicationContext(rootContext);

 protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
Class<?> contextClass = getContextClass();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Servlet with name '" + getServletName() +
"' will try to create custom WebApplicationContext context of class '" +
contextClass.getName() + "'" + ", using parent context [" + parent + "]");
}
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment());
wac.setParent(parent);
wac.setConfigLocation(getContextConfigLocation()); configureAndRefreshWebApplicationContext(wac); return wac;
}

如果不通过init-param修改dispatcherServlet的属性的话.contextClass默认是XmlWebApplicationContext.class

所以通过BeanUtils通过反射来创建XmlWebApplicationContext(applicationContext).

然后各种setter方法以后调用configureAndRefreshWebApplicationContext方法

 protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(getServletContext().getContextPath()) + "/" + getServletName());
}
} wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener())); // The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
} postProcessWebApplicationContext(wac);
applyInitializers(wac);
wac.refresh();
}

然后又是各种setter方法.

18行这里添加了一个eventListener,当xmlWebapplicationContext refresh(创建各种bean?)以后会通知这个listener.

     private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

         @Override
public void onApplicationEvent(ContextRefreshedEvent event) {
FrameworkServlet.this.onApplicationEvent(event);
}
}

而这个listener会delegate另外1个linstener ContextRefreshListener来处理.

而ContextRefreshListener是FrameworkServlet的内部类.因为是内部类.他可以调用FrameworkServlet的方法(onApplicationEvent方法).

     public void onApplicationEvent(ContextRefreshedEvent event) {
this.refreshEventReceived = true;
onRefresh(event.getApplicationContext());
}

onRefresh又是一个扩展接口.这个时间点是xmlWebapplicationContext finishRefresh的时候.

这个方法被dispatcherServlet用到了.后面会说.

25行 ConfigurableWebEnvironment的(实际上是StandardServletEnvironment)的initPropertySources方法具体做的事情是:

Replace Servlet-based stub property sources with actual instances populated with the given servletContext and servletConfig objects.

This method is idempotent with respect to the fact it may be called any number of times but will perform replacement of stub property sources with their corresponding actual property sources once and only once.

这我没太看懂...根据我的理解就是有些properties一开始在servletContext或者servletConfig还没有的时候会先用占位符properties去占位,然后现在通过这个方法替换成真正的包含servletContext或者servletConfig的properties.但是SpringMVC初始化的时候这些都是有了的...到底什么时候会存在这种情况我就不知道了..说不定Spring初始化xmlWebApplicationContext的时候会遇到吧.毕竟那个时候还是没有servletConfig的....

然后28行又是一个扩展点.

postProcessWebApplicationContext(wac);我们可以像先前那样,通过继承dispatcherServlet来实现.

这个时间点就是applicationContext还没有refresh(生成各种bean?)的时候.

然后又是1个扩展点:

applyInitializers(wac);

     protected void applyInitializers(ConfigurableApplicationContext wac) {
String globalClassNames = getServletContext().getInitParameter(ContextLoader.GLOBAL_INITIALIZER_CLASSES_PARAM);
if (globalClassNames != null) {
for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
this.contextInitializers.add(loadInitializer(className, wac));
}
} if (this.contextInitializerClasses != null) {
for (String className : StringUtils.tokenizeToStringArray(this.contextInitializerClasses, INIT_PARAM_DELIMITERS)) {
this.contextInitializers.add(loadInitializer(className, wac));
}
} AnnotationAwareOrderComparator.sort(this.contextInitializers);
for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
initializer.initialize(wac);
}
}

可以通过在web.xml里配置globalInitializerClasses和contextInitializerClasses来初始化applicationContext

比如公司的做法:

     <context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>XXXX.spring.SpringApplicationContextInitializer</param-value>
</context-param>
 public class SpringApplicationContextInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> { /**
* The Constant LOGGER.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(SpringApplicationContextInitializer.class); /* (non-Javadoc)
* @see org.springframework.context.ApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext)
*/
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
try {
ResourcePropertySource rps = new ResourcePropertySource(
applicationContext.getResource("classpath:application-cfg.properties"));
applicationContext.getEnvironment().getPropertySources().addFirst(rps);
} catch (IOException e) {
LOGGER.error("Fail to add properties in application-cfg.properties to environment", e);
}
}
}

通过编码方式强行加载application-cfg.properties

最后就是调用applicationContext的refresh方法了:

wac.refresh(); 容器的refresh里面也有很多很多东西.但是和SpringMVC好像没啥关系..和Spring有关.我就没细看了..

DispatcherServlet

前面提到在frameworkServlet初始化applicationContext finish的时候会触发监听器的事件,然后调用DispatcherServlet的onRefresh方法.

 /**
* This implementation calls {@link #initStrategies}.
*/
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
} /**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

这个方法要做的就是各种init....各种init方法里面都是差不多的.我们来看一个就可以了.

 /**
* Initialize the HandlerMappings used by this class.
* <p>If no HandlerMapping beans are defined in the BeanFactory for this namespace,
* we default to BeanNameUrlHandlerMapping.
*/
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null; if (this.detectAllHandlerMappings) {
// 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<HandlerMapping>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
OrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
} // Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isDebugEnabled()) {
logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
}
}
}

detectAllHandlerMappings默认是true.所以会去applicationContext里找HandlerMapping.class类型的bean.因为配置过<mvc:annotation-driven>所以可以加载到3个bean..

把dispatcherServlet里的成员变量指向这个HandlerMapping的list.因为后面分发请求的时候要用到.弄成成员域就不需要每次都去applicationContext里找了.

3个bean到底使用哪个呢? 先来排个序... 这个3个bean都实现了Ordered接口...排序结果不用多说大家肯定知道.RequestMappingHanddlerMapping肯定是最优先的...

后续

然后...然后以要做的后续事情就不多了...回到FrameworkServlet.把applicationContext放到ServletContext.与Spring初始化applicationContext的一样..

然后貌似就真的没有然后了....

思考与收获

这次学习学到了什么:

1. DispatcherServlet里有很多扩展点:

DispatcherServlet提供的扩展方法至少有(可能还有些我没发现):

(1) initFrameworkServlet(),在xmlWebApplicationContext初始化完成后(refresh完成后)调用

(2) postProcessWebApplicationContext(wac);在xmlWebApplicationContext各种set以后但是xmlWebApplicationContext还没有refresh(生成bean?)的时候调用

(3) onRefresh(ApplicationContext context);在xmlWebApplicationContext内部被调用,时间点是finish refresh的时候(bean初始化完成?).

DispatcherServlet指明的通过接口实现的扩展点至少有(可能还有些我没发现):

(1) 配置globalInitializerClasses或contextInitializerClasses,自己实现 ApplicationContextInitializer<C extends ConfigurableApplicationContext>接口,时间点是xmlWebApplicationContext refresh方法调用之前,postProcessWebApplicationContext(wac);之后.

2.结合上一篇文章,我知道了DispatcherServlet其实也并不是很特殊的,SpringMVC的@Controller自动委派请求等功能还是通过配置Bean(比如mvc:annotation-driven标签),让Bean参与Spring Bean的声明周期来完成的.所以这些事情是在xmlWebApplicationContext的refresh的时候发生的.并不是在DispatcherServlet里实现的.所以要搞清楚这些奇特的bean需要研究这些bean到底参与了bean声明周期的哪些步骤,到底做了写什么.此外还需要自己研究下xmlWebApplicationContext的refresh到底干了些什么事情.DispatcherServlet的initStrategies里的各种resolver,每种resolver都是为request与response的某一个环节服务的.比如RequestMappingHandlerMapping是为了让请求委派给正确的Controller服务的.研究这些resolver可以了解从解析请求到生成响应的细节.

3.对SpringMVC初始化的步骤有了了解.

SpringMVC学习记录2的更多相关文章

  1. springMVC学习记录1-使用XML进行配置

    SpringMVC是整个spring中的一个很小的组成,准确的说他是spring WEB这个模块的下一个子模块,Spring WEB中除了有springMVC还有struts2,webWork等MVC ...

  2. SpringMVC学习记录5

    Springmvc流程中的扩展点有很多,可以在很多地方插入自己的代码逻辑达到控制流程的目的. 如果要对Controller的handler方法做统一的处理.我想应该会有很多选择,比如:@ModelAt ...

  3. SpringMVC学习记录4

    主题 SpringMVC有很多很多的注解.其中有2个注解@SessionAttributes @ModelAttribute我平时一般不用,因为实在是太灵活了.但是又有一定限制,用不好容易错.. 最近 ...

  4. SpringMVC学习记录3

    这次的主题 最近一直在学习SpringMVC..(这句话我已经至少写了3,4遍了....).这次的研究主要是RequestMappingHandlerAdapter中的各种ArgumentsResol ...

  5. SpringMVC学习记录

    1E)Spring MVC框架 ①Jar包结构: docs+libs+schema. 版本区别:核心包,源码包. SpringMVC文档学习: 学习三步骤: 1)是什么? 开源框架 2)做什么? IO ...

  6. springMVC学习记录2-使用注解配置

    前面说了一下使用xml配置springmvc,下面再说说注解配置.项目如下: 业务很简单,主页和输入用户名和密码进行登陆的页面. 看一下springmvc的配置文件: <?xml version ...

  7. SpringMVC学习记录1

    起因 以前大三暑假实习的时候看到公司用SpringMVC而不是Struts2,老司机告诉我SpringMVC各种方便,各种解耦. 然后我自己试了试..好像是蛮方便的.... 基本上在Spring的基础 ...

  8. springMVC学习记录3-拦截器和文件上传

    拦截器和文件上传算是springmvc中比较高级一点的内容了吧,让我们一起看一下. 下面先说说拦截器.拦截器和过滤器有点像,都可以在请求被处理之前和请求被处理之到做一些额外的操作. 1. 实现Hand ...

  9. SpringMVC学习记录七——sjon数据交互和拦截器

    21       json数据交互 21.1      为什么要进行json数据交互 json数据格式在接口调用中.html页面中较常用,json格式比较简单,解析还比较方便. 比如:webservi ...

随机推荐

  1. Oracle forall bulk collect批量数据更新

    对于数据量较大的插入操作可采用此种方法操作,注意: limit减少内存占用,如果数据量较大一次性全部加载到内存中,对PGA来说压力太大,可采用limit的方法一次加载一定数量的数据,建议值通常为100 ...

  2. x86开启 HUGEPAGES

    HugePage,就是指的大页内存管理方式,在操作系统Linux环境中,内存是以页Page的方式进行分配,默认大小为4K,HugePage是传统4K Page的替代方案.顾名思义,是用HugePage ...

  3. 【转】XenServer的架构之Xenopsd组件架构与运行机制

    一.Xenopsd概述 Xenopsd是XenServer的虚拟机管理器. Xenopsd负责:启动,停止,暂停,恢复,迁移虚拟机:热插拔虚拟磁盘(VBD):热插拔虚拟网卡(VIF):热插拔虚拟PCI ...

  4. python爬虫学习(6) —— 神器 Requests

    Requests 是使用 Apache2 Licensed 许可证的 HTTP 库.用 Python 编写,真正的为人类着想. Python 标准库中的 urllib2 模块提供了你所需要的大多数 H ...

  5. idea缓存

    昨天idea出现了一个奇怪的问题: 项目没有按我指定的配置运行,按cmd+:可以看输出.而是运行了配置包下的test环境的配置, 先一看,test环境被初始化为资源包并且在输出目录上, 先取消(fil ...

  6. Android客户端和服务器端数据交互

    网上有很多例子来演示Android客户端和服务器端数据如何实现交互不过这些例子大多比较繁杂,对于初学者来说这是不利的,现在介绍几种代码简单.逻辑清晰的交互例子,本篇博客介绍第四种: 一.服务器端: 代 ...

  7. float4数据类型

    GPU是以四维向量为基本单位来计算的.4个浮点数所组成的float4向量是GPU内置的最基本类型.使用GPU对两个float4向量进行计算,与CPU对两个整数或两个浮点数进行计算一样简单,都是只需要一 ...

  8. There is no getter for property named 'useName' in 'class cn.itcast.mybatis.pojo.User'

    org.apache.ibatis.exceptions.PersistenceException: ### Error updating database.  Cause: org.apache.i ...

  9. IDEA 中生成 Hibernate 逆向工程实践

    谈起 Hibernate 应该得知道 Gavin King 大叔,他构建了 Hibernate ,并将其捐献给了开源社区. Hibernate 对象关系映射解决方案,为面向对象的领域模型到传统的关系型 ...

  10. 基于C/S架构的3D对战网络游戏C++框架 _01服务器端与客户端需求分析

    本系列博客主要是以对战游戏为背景介绍3D对战网络游戏常用的开发技术以及C++高级编程技巧,有了这些知识,就可以开发出中小型游戏项目或3D工业仿真项目. 笔者将分为以下三个部分向大家介绍(每日更新): ...