springmvc更多家族成员----handler与handleradaptor---07(代码片段)

大忽悠爱忽悠 大忽悠爱忽悠     2022-11-30     299

关键词:

Spring MVC更多家族成员----Handler与HandlerAdaptor---07


问题的起源

最初为了降低理解的难度,我们说,HandlerMapping将会通过HandlerExecutionchain返回一个Controller用于具体Web请求的处理。

public interface HandlerMapping 
     ...  
	@Nullable
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

现在我们要进一步澄清事实:HandlerExecutionchain中所返回的用于处理Web请求的处理对象,可以不只是Controller一种类型。

在Spring MVC中,任何可以用于Web请求处理的处理对象统称为Handler。Controller是Handler的一种特殊类型。

HandlerMapping通过HandlerExecutionChain所返回的是一个Object类型的Handler对象,而并没限定说只能是Controller类型。

所以,一般意义上讲,任何类型的Handler都可以在Spring MVC中使用,比如Struts的Action和WebWork的Action等,只要它们是用于处理Web请求的处理对象就行。

不过,对于DispatcherServlet来说,这就有点儿问题了,它如何来判断我们到底使用的是什么类型的Handler,又如何决定调用Handler对象的哪个方法来处理Web请求呢?

显然,在DispatcherServlet直接硬编码if-else来枚举每一种可能的Handler类型是不具任何扩展性的。

为了能够以统一的方式调用各种类型的Handler,DispatcherServlet将不同Handler的调用职责转交给了一个称为HandlerAdaptor的角色。

public interface HandlerAdapter 
	boolean supports(Object handler);
	
	@Nullable
	ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
	
	@Deprecated
	long getLastModified(HttpServletRequest request, Object handler);

实际上,人如其名。哦,不对!是“接口”如其名。HandlerAdaptor将作为一个适配器,屏蔽不同Handler类型给DispatcherServlet)所造成的“困扰”(所谓Adaptor Pattern应该也就是为了这种类似的目的吧)

HandlerAdapter得以成为DispatcherServlet和不同Handler的“中间人”,要归功于它的两个主要方法,即supports(…)和handle(…)。

至于getLastModified(…)方法,它的主要目的只是为返回给客户端的Last-Modified这个HTTP头提供相应的时间值。如果我们不想支持该功能,直接返间-1即可。

DispatcherServlet从HandlerMapping获得一个Handler之后,将询问HandlerAdaptor的supports(…)方法,以便了解当前HandlerAdaptor是否支持HandlerMapping刚刚返回的Handler类型的调用。

                ....
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) 
					noHandlerFound(processedRequest, response);
					return;
				
                ....
				//寻找上面返回的handler对应的HandlerAdapter 
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

如果supports(…)返回true,DispatcherServlet则调用HandlerAdaptor的Handle方法,同时将刚才的Handler作为参数传入。

	protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException 
		if (this.handlerAdapters != null) 
			for (HandlerAdapter adapter : this.handlerAdapters) 
				if (adapter.supports(handler)) 
					return adapter;
				
			
		
		throw new ServletException("No adapter for handler [" + handler +
				"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
	

方法执行后将返回ModelAndview,之后的工作就由ViewResolver接手了。

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = HttpMethod.GET.matches(method);
				if (isGet || HttpMethod.HEAD.matches(method)) 
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) 
						return;
					
				

				if (!mappedHandler.applyPreHandle(processedRequest, response)) 
					return;
				

				//通过HandlerAdapter的handle方法返回一个ModelAndView
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

现在,DispatcherServlet只需要面对HandlerAdaptor提供的统一接口,而不需要面对纷繁复杂的Handler类型。支持新的Handler类型,意味着只需要为DispatcherServlet提供新的HandlerAdaptor实现即可。

DispatcherServlet借助多个HandlerAdaptor,调用当前HandlerMapping所返回的Handler:来处理Web请求的逻
辑。


无论我们想在Spring MVC中使用什么类型的Handler,只要同时为DispatcherServlet:提供对应该Handler的HandlerAdaptor实现就成,DispatcherServlet无须任何变动。

我想,HandlerAdaptor存在的原因我们已经搞清楚了,但还有一些问题不明,比如:

  • 如果Controller只是一种特殊类型的Handler,那么Spring MVC是否还提供了其他可用的Handler类型呢?如果要提供我们自己的Handler类型又需要考虑哪些事情呢?
  • 如何实现一个具体的HandlerAdaptor? Spring MVC有提供现成的实现吗?
  • 如果想使用自定义的Handler,并且提供了对应的HandlerAdaptor实现,要通过什么方式告知DispatcherServlet来使用它们?

我想只有解开以上问题的答案才能帮助我们更深刻地理解Handler与HandlerAdaptor之间的关系。


深入了解Handler

到目前为止,我们使用最多的Handler就是Controller。不过,如果Controller不合我们的口味的话,我们也可以使用Spring MVC提供的其他类型的Handler,甚至于自定义Handler类型。

自定义Handler

可以说,自定义Handler对于Handler类型来说并没任何限制,任何我们喜欢的形式都可以。如果不喜欢Controller,那么可以定义自己的MyHandler,甚至不需要强制Handler实现任何接口,仅是一个简单的POJO对象,只要能有办法知道该类就是用于Web请求处理的Handler类就行,比如用注解标注一下,然后通过反射机制就能获知哪些对象是用于Web请求处理的Handler,如下所示:

@Handler
public class AnyType

虽说对Handler自身没有任何限制,但是要让我们的Handler登上“历史舞台”发挥它的作用,却需要有能够给予帮助的“左膀右臂”,为我们的Handler提供必要的HandlerMapping和HandlerAdaptor,这才是真正让Handler自身没有任何限制的原因所在。

HandlerMapping负责查找相应的Handler以处理Web请求。

要想使用Handler,首先需要提供一个能够识别该Handler的HandlerMapping实现。比如,无论是BeanNameUrlHandlerMapping还是SimpleUrlHandlerMapping,它们都可以获取并返回Controller类型的Handler.。

如果可以通过BeanNameUrlHandlerMapping或者SimpleUrlHandlerMapping告知DispatcherServlet我们的Handler存在的话,那还好。

否则,我们就不得不提供一个能够识别我们自己Handler类型的HandlerMapping。

Spring2.5就提供了特定的DefaultAnnotationHandlerMapping,处理新提供的基于注解的Handler的查找。

现在HandlerMapping返回了我们自定义的Handler,但DispatcherServlet本身显然是不管我们的Handlerl到底是何方人物的。为了让我们的Handler得以被DispatcherServlet所“青睐”,我们不得不提供一个HandlerAdaptor。实际上,并非只有我们的自定义Handler要“受此礼遇”,所有SpringMVC框架内的Handler都提供有相对应的HandlerAdaptor实现,如下所述。

  • Controller 将 org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter作为其HandlerAdaptor。
  • Spring2.5新添加的基于注解的Handler由org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter作为其HandlerAdaptor。

这是一个“传统”,我们也得遵守。不过话又说回来了,保持框架设计的统一性确实非常重要。说了这么多,你对HandlerAdaptor可能还是没有一个感性的认识。别急,马上为你奉上。


近看HandlerAdaptor的奥秘

实际上,为具体的Handler类型提供一个HandlerAdaptor实现类非常简单。

主要工作只是调用这个HandlerAdaptor“认识”的Handler的Web请求处理方法,然后将处理结果转换为DispatcherServlet统一使用的ModelAndview就行。

我们不妨就以Controllerl的HandlerAdaptor为例,来看一下HandlerAdaptor实现到底长什么样子。

public class SimpleControllerHandlerAdapter implements HandlerAdapter 

	@Override
	public boolean supports(Object handler) 
		return (handler instanceof Controller);
	

	@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception 

		return ((Controller) handler).handleRequest(request, response);
	

	@Override
	@SuppressWarnings("deprecation")
	public long getLastModified(HttpServletRequest request, Object handler) 
		if (handler instanceof LastModified) 
			return ((LastModified) handler).getLastModified(request);
		
		return -1L;
	


SimpleControllerHandlerAdapter是Controller对应的HandlerAdaptor实现。

supports方法决定了它只认识Controller一个人,所以,在handle方法中,直接将object类型的Handler强制转型为Controller,然后调用其handleRequest即可。因为Controller的handleRequest方法可以返回已经组装好的ModelAndview,所以,就直接返回了。


告知Handler与HandlerAdaptor的存在

我们已经有了要添加到Spring MVC框架的新的Handler类型,也给出了针对该Handler类型的
HandlerAdaptor实现类,现在是让DispatcherServlet接纳它们的时候了!

首先,如果现有的HandlerMapping不足以“感知”到我们的Handler类型的话,那么我们需要提供一个能够“感知”我们Handler的HandlerMapping实现类,并将其注册到DispatcherServlet的WebApplicationContext中。如果现有的HandlerMapping实现可以“感知”到我们的Handler,那么将可以省去实现自定义HandlerMapping的工作,但依然需要将使用的HandlerMapping添加到DispatcherServlet的WebApplicationContext中,除非我们使用默认的BeanNameUrlHandlerMapping。

大部分情况下,只要我们提供的Handler在容器中的引用,能够明确指定给BeanNameUrlHandlerMapping.或者SimpleUrlHandlerMapping等现有HandlerMapping实现类,都不需要自定义HandlerMapping实现。

当然,在稍后详细介绍Spring2.5新添加的基于注解的Handler实现的时候,我们将看到第一个为特定Handler提供的HandlerMapping实现。

其次,有了可以返回我们自定义的Handler的HandlerMapping之后,我们要为DispatcherServlet
提供能够调用该类型Handlerf的HandlerAdaptor实现。

这同样是通过将HandlerAdaptor实现类添加到WebApplicationContext完成的。

可以向DispatcherServlet的WebApplicationContext中添加多个HandlerAdaptor。

DispatcherServlet将根据类型自动检测容器内可用的HandlerAdaptor实例。如果无法找到可用的HandlerAdaptor,DispatcherServlet将启用后备的几个默认使用的HandlerAdaptor实现。

	private void initHandlerAdapters(ApplicationContext context) 
		this.handlerAdapters = null;
        //默认会去搜索容器内所有类型为HandlerAdapter的Bean 
		if (this.detectAllHandlerAdapters) 
			Map<String, HandlerAdapter> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
			//如果用户往容器中放入了相关HandlerAdapter实现Bean		
			if (!matchingBeans.isEmpty()) 
				this.handlerAdapters = new ArrayList<>(matchingBeans.values());
				//排序---order接口
				AnnotationAwareOrderComparator.sort(this.handlerAdapters);
			
		
		else 
			try 
			   //如果我们将detectAllHandlerAdapters设置为false,那么默认只会去容器中
			   //寻找beanName为handlerAdapter的Bean
				HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
				this.handlerAdapters = Collections.singletonList(ha);
			
			catch (NoSuchBeanDefinitionException ex) 
				// Ignore, we'll add a default HandlerAdapter later.
			
		

		//如果没找到,那么采用默认的策略
		if (this.handlerAdapters == null) 
			this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
			if (logger.isTraceEnabled()) 
				logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
						"': using default strategies from DispatcherServlet.properties");
			
		
	

加载默认策略:

	protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) 
	//第一次进来时,首先初始化
		if (defaultStrategies == null) 
			try 
			//加载DispatcherServlet类路径下的DispatcherServlet.properties文件
				ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
				//加载该文件
				defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
			
			catch (IOException ex) 
				throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
			
		
        //key就是要寻找的默认类路径---这里对应上面为handlerAdapter的全类名
		String key = strategyInterface.getName();
		//去配置文件中定位默认组件---如果存在多个,使用逗号分割
		String value = defaultStrategies.getProperty(key);
		if (value != null) 
		//如果默认配置存在的话,按照逗号分割,拿到所有默认实现类的每一个全类名
			String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
			List<T> strategies = new ArrayList<>(classNames.length);
			for (String className : classNames) 
				try 
					Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
					//实例化后,放入容器中
					Object strategy = createDefaultStrategy(context, clazz);
					strategies.add((T) strategy);
				
				catch (ClassNotFoundException ex) 
					throw new BeanInitializationException(
							"Could not find DispatcherServlet's default strategy class [" + className +
							"] for interface [" + key + "]", ex);
				
				catch (LinkageError err) 
					throw new BeanInitializationException(
							"Unresolvable class definition for DispatcherServlet's default strategy class [" +
							className + "] for interface [" + key + "]", err);
				
			
			return strategies;
		
		else 
			return Collections.emptyList();
		
	

当某个组件在进行初始化时,如果用户没有自定义的话,那么就采用配置文件中提供的默认值,如果默认组件存在多个,就使用","分割。


也就是说,如果使用的Handler是现有Handler类型,那么无须在DispatcherServlet的WebApplicationContext中做任何配置,默认的HandlerAdaptor已经足够了。

不过,如果需要添加这些HandlerAdaptor类型之外的HandlerAdaptor实现,并且我们依然希望同时使用这些默认HandlerAdaptor所支持的Handler的话,那就需要在添加我们的自定义HandlerAdaptor的基础上,同时添
加以上几种默认的HandlerAdaptor实现。


案例

假设Spring没有提供注解版本的Controller,现在需要我们自己来进行实现,你会怎么做呢?

思路如下:

  • 自定义一个AnnoHandlerMapping扫描容器内所有标注了@Controller注解的Bean,然后将每个Controller能够处理的请求路径和自身做好映射
  • 请求来临时,通过AnnoHandlerMapping中保存的映射关系,判断哪一个Controller来处理当前请求
  • 当请求交给Controller之后,Controller内部再进行解析,判断具体应该交给哪个方法进行处理
  • Controller内部方法处理完毕后,将返回的值包装为ModelAndView返回

默认返回值通通作为JSON处理

  • AnnoHandlerMapping封装映射关系
public class AnnoHandlerMapping extends AbstractHandlerMapping implements InitializingBean 
   //封装扫描到的所有标注了@Controller注解的handler
    private static List<ControllerHolder> controllerHolders;


    @Override
    public void afterPropertiesSet() throws Exception 
        ApplicationContext app = getApplicationContext();
        if (app != null) 
            //扫描+解析
            Set<Map.Entry<String, Object>> handlers = app.getBeansWithAnnotation(Controller.class).entrySet();
            controllerHolders=new ArrayList<>();
            for (Map.Entry<String, Object> handler : handlers) 
                controllerHolders.add(ControllerHolder.parserController(handler.getValue()));
            
        
    

   
    @Override
    protected Object getHandlerInternal(HttpServletRequest request) throws Exception 
        String reqURI = request.getRequestURI();
        for (ControllerHolder controllerHolder : controllerHolders) 
            Method method = controllerHolder.getMappings().get(reqURI);
            if(method==null)
                continue;
            
            //设置当前正在执行的方法和方法实参列表值
            controllerHolder.setCurMethod(method);
            controllerHolder.setParamsVal(request);
            //返回ControllerHolder 
            return controllerHolder;
        
        return null;
    


    @Data
    public static class ControllerHolder
       //封装目标对象
        private Object controller;
      //处理当前请求的方法
        private Method curMethod;
     //方法实参    
        private Object[] paramsVal;
    //当前目标handler对象中请求路径和方法的映射关系 
        private Map<String,Method> mappings;
    //方法和当前方法中参数信息封装     
        private Map<Method,List<MethodParam>> methodParams;
     //用于类型转换
         private TypeConverter typeConverter=new SimpleTypeConverter();

        public ControllerHolder(Object controller, Map<String, Method> mappings, Map<Method,List<MethodParam>> methodParams) 
            this.controller = controller;
            this.mappings = mappings;
            this.methodParams=methodParams;
        
       
       //解析controller对象   
        public static ControllerHolder parserController(Object controller)
            Method[] methods = controller.getClass().getMethods();
            Map<String,Method> mappings=new HashMap<>();
            Map<Method,List<MethodParam>> methodParams=new HashMap<>();
            //解析controller对象中所有方法
            for (Method method : methods) 
                //获取方法上标注的@RequestMapping注解
                RequestMapping req = method.getAnnotation(RequestMapping.class);
                //没有标注该注解的方法不会被作为处理请求的方法
                if(req==null)
                    continue;
                
                //当前方法和对应请求的映射
                mappings.put(req.value()[0],method);
                //当前方法和对应方法参数信息的封装
                methodParams.put(method,parseMethodParam(method));
            
            return new ControllerHolder(controller,mappings,methodParams);
        

        private static List<MethodParam> parseMethodParam(Method method) 
            List<MethodParam> paramNames=new ArrayList<

springmvc更多家族成员---框架内处理流程拦截与handlerinterceptor---08(代码片段)

SpringMVC更多家族成员---框架内处理流程拦截与HandlerInterceptor---08引言preHandlepostHandleafterCompletionHandlerExecutionChain源码概览可用的HandlerInterceptor实现UserRoleAuthorizationInterceptorWebContentInterceptor自定义HandlerIn 查看详情

springmvc更多家族成员----handler与handleradaptor---07(代码片段)

SpringMVC更多家族成员----Handler与HandlerAdaptor---07问题的起源深入了解Handler自定义Handler近看HandlerAdaptor的奥秘告知Handler与HandlerAdaptor的存在案例问题的起源最初为了降低理解的难度,我们说,HandlerMapping将会通过HandlerExecutionc... 查看详情

springmvc更多家族成员--国际化视图与localresolver---10(代码片段)

SpringMVC更多家族成员--国际化视图与LocalResolver---10引言可用的LocaleResolverLocaleResolver的足迹LocaleResolver在初始化流程中的使用processRequest处理请求的核心方法DispatcherServlet的doservice方法小结LocaleResolver后继使用时机体会Locale的变更与L... 查看详情

springmvc更多家族成员--主题(theme)与themeresolver(代码片段)

SpringMVC更多家族成员--Theme与ThemeResolver引言提供主题资源的ThemeSource管理主题的ThemeResolver切换主题的ThemeChangeInterceptor引言不管是使用Windows操作系统还是使用Linux操作系统,当我们对某种风格的桌面主题感到厌烦的时候,... 查看详情

springboot与springmvc的区别

参考技术ASpringMVC和SpringBoot都是Spring家族的重要成员。Spring家族的使命就是为了简化而生。SpringMVC简化日常Web开发的,后来随着自身的发展,SpringMVC变得臃肿复杂,而SpringBoot则进一步简化了SpringMVC开发。SpringMVC为JavaWeb而生。Sprin... 查看详情

hadoop之家族成员pig简介

...许多的子项目,今天的内容就是简单的介绍一下Hadoop家族的子项目中的Pig。下图是一个Hadoop子项目的大体结构图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-INUf7iRp-1662106189048)(http://172.18.3.4:8090/... 查看详情

spring-boot-starter家族成员简介

以下应用程序starters是SpringBoot在org.springframework.boot组下提供的:名称描述spring-boot-starter核心SpringBootstarter,包括自动配置支持,日志和YAMLspring-boot-starter-actuator生产准备的特性,用于帮我们监控和管理应用spring-boot-starter-amqp对”... 查看详情

spring-boot-starter家族成员简介

下应用程序starters是SpringBoot在org.springframework.boot组下提供的:名称描述spring-boot-starter核心SpringBootstarter,包括自动配置支持,日志和YAMLspring-boot-starter-actuator生产准备的特性,用于帮我们监控和管理应用spring-boot-starter-amqp对”... 查看详情

intellijideaultimate家族新成员bigdatatools——集成zeppelin和spark

...除段落运行段落浏览段落的输出支持基本的可视化将会有更多的语言和集成。路线图即将发布,敬请关注。快速说明确保版本为IntelliJIDEAUltimate2019.2* 确保已安装BashSupport,Python和Scala插件在设置|插件,切换到“市场”选项卡,... 查看详情

C++11 'native_handle' 不是 'std::this_thread' 的成员

】C++11\\\'native_handle\\\'不是\\\'std::this_thread\\\'的成员【英文标题】:C++11\'native_handle\'isnotamemberof\'std::this_thread\'C++11\'native_handle\'不是\'std::this_thread\'的成员【发布时间】:2013-04-2803:22:29【问题描述】:在下面的代码sn-p中,voidfoo()s 查看详情

dcm:中间件家族迎来新成员(代码片段)

文章目录DCM是什么DCM应用场景优化应用开发多样性数据源计算微服务实现存储过程替代报表BI数据准备中间表消除T+0查询ETLDCM特性兼容性(Compatible)热部署(Hot-deploy)高性能(Efficient)敏捷性(Agile... 查看详情

javascript之--offset家族

  在javavscriprt中,有一些很有特点的家族,他们有的是因为浏览器的兼容问题而出名,有的是因为其重要的作用而出名,总之对这些pinyi家族的理解和解决他们的兼容性问题显得很是繁琐,这篇文章就是对家族成员之一offs... 查看详情

小点心家族第3位成员——楼层定位效果

ES6小点心,顾名思义,开箱即食,拿来即用。献上第3个小点心:Floor。GitHub在线演示觉得有用记得GitHub点个star哟^_^【楼层定位】是前端日常开发中的常见需求。这个效果本身并没有什么高深的地方,不出意外的话,一般Jser在几... 查看详情

.btc勒索病毒删除+还原文件(dharma家族新成员)

新变种中检测到Dharma勒索病毒,这次将.btc文件扩展名附加到由其加密的文件中。勒索病毒类似于Dharma的其他变体,旨在利用多种加密模式的组合,以便使受害者计算机上的文件不再能够打开并显示如下:Filename.idID-here.[ [email... 查看详情

[中文]orangepi家族各大成员一览表

全部复制来自官网,只修改了错别字和优化了排版。OrangePiZero香橙派Zero是一款开源的单板电脑,新一代的arm开发板,它可以运行Android4.4、Ubuntu、Debian等操作系统。香橙派Zero使用全志H2系统级芯片,同时拥有256MB/512MBDDR3内存(256MB... 查看详情

ssm框架springmvc笔记---汇总

SpringMVC是基于MVC开发模式的框架,用来优化控制器,是Spring家族的一员,同时它也具备IOC和AOPSpringMVC是基于MVC开发模式的框架,用来优化控制器,是Spring家族的一员,同时它也具备IOC和AOP 查看详情

dcm:中间件家族迎来新成员,属实牛逼(代码片段)

您好,我是码农飞哥,感谢您阅读本文,欢迎一键三连哦。💪🏻1.Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。Python从入门到精通❤️2.Python爬虫专栏,系统性的学习爬虫... 查看详情

dcm:中间件家族迎来新成员,属实牛逼(代码片段)

您好,我是码农飞哥,感谢您阅读本文,欢迎一键三连哦。💪🏻1.Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。Python从入门到精通❤️2.Python爬虫专栏,系统性的学习爬虫... 查看详情