查看原文
其他

Java Web开发API Boy如何进阶?

​王展雄 云加社区 2022-06-14
导语 | Java Web后台开发基本都离不开Spring生态这一套,Spring框架功能极其强大,会帮忙做许多工作,我们通常只需要在一个函数(包含request和response两个入参)中写处理逻辑即可。但是想要真正写好工业级的Java Web应用,对于开发细节必须有更深入的了解,比如一个Http请求发过来之后,解析请求的代码在哪里,是不是Spring框架做的解析?写的业务逻辑代码没有main函数入口,是如何被调用执行的?开发中用到的Listerner,Filter,Interceptor到底是什么,属于哪一个框架,怎么工作的?定义一个Java类,如何变成Spring IoC容器中的对象?面向切面编程的AOP到底怎么生效的?注解是怎么生效的?只有搞清楚了这些,理解了原理,开发起来才能有积累,不仅仅做一个API Boy。本文首先会介绍Java Web中用到的一些技术,然后简单介绍一下Spring框架完成了Java Web中的哪些功能,最后介绍Spring Boot开发的流程,以及Spring Cloud的学习资料链接。

一、JavaWeb技术


1. Servlet


首先介绍JavaWeb开发中的第一个名词Servlet。



通过维基百科上的介绍,我们知道:


1)严格定义的Servlet是一个Java的接口,并未实现,仅仅定义了Web服务器响应请求的一些方法。
2)广义的Servlet是指,实现了这个接口方法的一个Java类。

3)Servlet运行在Web服务器(比如Tomcat)中,当Web服务器接收到一个Http请求,Web服务器解析出request,并生成一个空的response,把这两个参数传给Servlet,接下来在Servlet中进行逻辑处理,最终把结果数据response给到Web服务器,再响应给请求端。


2. Jsp


了解了Servlet之后,不得不提的一个名词是Jsp,虽然现在前后端分离的Web开发方式下,已经比较少听到Jsp了,不过还是值得我们了解一下。



从浏览器请求一个HTML网页文件,可以渲染出一个静态网页。通过维基百科上的介绍,我们知道:


1)Jsp文件是一个类似于HTML文件的文件,不过其中可以写一些Java代码。
2)Jsp文件中,HTML部分为静态模版,Java代码部分可以动态获取一些数据填充在模版中。
3)Jsp文件最终运行时候,还是被编译转换成了一个Servlet,由Servlet来实现动态获取数据的逻辑,并拼接生成最终的HTML文件。

4)Jsp可以根据客户端的请求,动态的生成HTML等Web网页,最终返回给浏览器渲染出网页。


3. Servlet容器(以Tomcat为例)



现在进行Java Web的开发,通常直接就上Spring MVC/Spring Boot框架了,在Controller里面就开始处理请求request→做逻辑→最后返回response,让初学者很容易混淆HTTP服务器,Servlet容器与Spring框架的关系。这里先介绍一个常见的Web服务器Apache Tomcat,它本身包含了HTTP服务器,所以可以响应HTTP请求;同时它实现了对Servlet和JSP的支持,即我们自己编写的Servlet类可以运行在Tomcat这个Servlet容器中。


介绍到这里,大家是否已经对Java Web开发流程有了一些更清晰的认识:


1)前端发送的Http请求到Tomcat,Tomcat中包含可以响应HTTP请求的HTTP服务器。


2)Tomcat先解析HTTP请求中的参数,new一个request对象并赋值,同时会new一个空的response对象。


3)再利用Tomcat的Servlet引擎,把我们写的用来做逻辑处理的Servlet类new成对象,并把request对象和response对象作为参数传入到Servlet的方法中。
  • 如果请求的是静态资源,比如HTML页面,Tomcat会用自带的DefaultServlet处理请求,并将资源直接返回给前端。(正规的项目通常不会用Tomcat做静态资源服务器,因为所有资源请求都会通过DefaultServlet处理,会占用大量线程,极大影响性能,通常会前置一个静态资源服务器(nginx,apache),不仅做负载均衡,还能够高效处理返回静态文件。)
  • 如果请求的是动态资源,则用的是我们自己写的Servlet进行处理。


4)Servlet类中进行逻辑处理,最后把结果数据set到response对象中。


5)再给回到Tomcat,再给回到前端。


所以我们用Spring/Spring Boot框架的时候,主要逻辑代码其实是在Servlet类及其调用类中,然后网络通信相关的工作是Web服务器帮忙做的。


Tomcat服务器 = Web服务器 + Servlet/JSP容器。


4. Listener


Java Web开发时候,主要逻辑代码的入口是Servlet的实现类,除此之外,还可以通过监听器Listener实现一部分逻辑。在JavaWeb中Listener是Servlet规范定义的一种特殊类,主要用于监听指定对象(JavaWeb三大域对象)的特定事件(创建、销毁以及其属性变更),并进行响应处理。Listener是Servlet提供的扩展功能接口,有了这些扩展功能接口,我们就可以比较方便实现一些功能,比如统计网站的同时在线人数,可以使用HttpSessionListener,当Session创建时在线人数+1,Session销毁时在线人数-1,如果没有提供Listener,实现起来就会更加复杂。


介绍Listener之前,先介绍一下JavaWeb的三大域对象,ServletContext、HttpSession、ServletRequest。域对象有点像大型的map,可以通过getAttribute()和setAttribute()方法,存储/获取数据。


  • ServletContext 域:
生命周期:当我们的Web应用被加载进Servlet容器时,Web服务器创建代表整个Web应用的ServletContext对象,当服务器关闭或Web应用被移除时,ServletContext对象跟着销毁。 

作用范围:整个Web应用(应用范围)。


  • ServletRequest 域:
生命周期:在Servlet的 service(request, response) 方法调用前,由Web服务器创建ServletRequest,然后传入service方法。整个请求结束,ServletRequest 生命结束。

作用范围:(请求范围)整个请求链(请求转发也存在)。


  • HttpSession 域:
生命周期:HttpSession在第一次调用 servletRequest.getSession() 方法时,服务器会检查是否已经有对应的session,如果没有就在内存中创建一个session并返回。

作用范围:一次会话。


在JavaWeb中Listener主要用于监听三大作用域的创建、销毁,以及其属性变更,常用的Listener有六个:
  • ServletContextListener

  • ServletContextAttributeListener

  • HttpSessionListener

  • HttpSessionAttributeListener

  • ServletRequestListener

  • ServletRequestAttributeListener


每当Tomcat创建或销毁三大域对象,三大域对象调用get/setAttribute()方法,都会被这些Listener发现,然后触发调用指定的方法。Listener的实现是利用“观察者模式(Observer)”,有兴趣的读者可以自行搜索一下。


5. Filter


Filter有什么用?我们先看一个业务场景,我们的Web应用,我们希望在用户访问之前,先进行用户身份的确认,任何一次的访问,不管是访问Servlet的动态资源,还是访问Html的静态资源,都需要进行身份的确认。


我们知道从3中知道,在访问动态资源的Servlet时,我们是可以前置一段权限校验的代码,但是每一个Servlet都需要加上这一段严重耦合的重复代码,非常不合理;在访问静态资源,会通过Tomcat自带的DefaultServlet进行处理,我们没有办法修改它,没有办法添加权限校验的代码。


所以这里就需要一个新的环节,位于用户请求和Servlet之间,每一个用户请求,都先经过这个环节,这个环节中可以进行逻辑处理,然后再决定请求是否到达Servlet,这个环节就是Filter


Filter的底层实现用到了责任链模式(FilterChain),有兴趣的同学可以自行查阅一下,可以将多个写好的Filter给串起来,请求访问的时候按照顺序执行。Filter在使用上是非常简单的,和Listener一样,只需要配置xml或者通过注解的方式配置,然后实现对应接口即可。Filter是属于Tomcat服务器的,Tomcat也有容器对象管理的功能,在初始化的时候,将配置好的Filter注册到FilterChain中。


介绍到这里,Java Web的开发流程就比较清晰了:


1)项目启动,Tomcat的Main()函数启动。
2)创建ServletContext。
3)读取web.xml,通过反射的方式创建Listener。
4)给ServletContext注入ServletContextListener。
5)ServletContext初始化时,我们写的Listener被执行。
6)读取web.xml,通过反射的方式创建Filter,并放入一个FilterChain中,等待请求到达。
7)请求到达,读取web.xml,通过反射的方式创建Servlet,创建request和response并传给Servlet。
8)执行FilterChain中的Filter,我们写的Filter被执行。

9)Servlet处理请求,响应结果,我们写的Servlet被执行。


6. Servlet映射器


Servlet还有一点可能会造成一些困惑的地方,我们知道在web.xml中配置<servlet-mapping>标签或者注解方式配置:



但是对于每一个请求的URL,是怎么对应上正确的Servlet的呢?具体的映射规则是由Tomcat中的Mapper类提供的,其中提供了精准匹配、前缀匹配、扩展名匹配等7种匹配规则,匹配之后:


  • 对于静态资源,Tomcat会交由一个叫做DefaultServlet的类来处理;
  • 对于JSP,Tomcat会交由一个叫做JspServlet的类来处理;
  • 对于Servlet ,Tomcat会交由一个叫做 InvokerServlet的类来处理。


现在在实际的开发中,已经不会直接利用Tomcat的Mapper类来做映射了,一般会利用Spring MVC模块中的HandlerMapping来做映射,可以参考下一节的第4部分。


7. Cookie与Session


由于客户端与服务端是通过HTTP协议通信,但HTTP是一种无状态协议,即客户端第二次来访时,服务端无法判断该客户端是否来访过。会话跟踪常用的有两种技术:Cookie和Session,Session底层依赖于Cookie


在学习Web开发之前,接触到Cookie就是,偶尔需要清除浏览器的Cookie缓存,大概知道Cookie是一个存在于客户端的缓存信息。实际也就是这样,Cookie是一份数据,是服务端响应给客户端,并且存储在客户端的一份小数据。下次客户端访问服务端时,会自动带上这个Cookie。Cookie也可以设置不同的保持方式和存活时间,比如存在浏览器内存(浏览器关闭即清除)或者可以持久化到磁盘等。这样访问同一个网站的不同页面,只需要登陆一次,其他页面都可以通过Cookie进行鉴权。


不过Cookie也有一些不足,比如将密码等敏感信息存在客户端会有安全问题,Cookie包含信息太多会有传输效率问题等,所以就有了Session。Session是存在于服务端的东西,像一个大Map,以键值对的形式存储数据。将之前要通过Cookie回传给客户端的数据保存在客户端的Session中,并生成一个JSESSIONID,此时只需要将JSESSIONID通过Cookie回传给客户端即可,客户端下次请求只需要带上JSESSIONID,即可在服务端找到对应的信息。


8. 跨域问题及解决方案


Ajax跨域问题,这个课程讲的非常详细了,简洁明了,大家可以直接观看:Ajax跨域完全讲解(https://www.imooc.com/learn/947)


二、Spring Framework



在了解了Java Web开发的底层技术之后,我们就可以通过使用框架来提升开发效率了。框架就是对这些底层技术进行一系列的封装,简化我们的开发流程,提升研发效率。Java企业应用开发,应用最广的应该就是Spring框架了。打开Spring的官网,可以看到Spring框架中包含了非常多的组件,这里我简单介绍一下Spring Framework中的部分核心能力,包括Spring Core(提供控制反转(IoC)和依赖注入(DI))、Spring AOP(提供面向切面编程能力)和Spring MVC(Spring Framework中的重要Web模块),简单来说,Spring是一个控制反转(IoC)和面向切面(AOP)的容器框架。


1. 控制反转(IoC)和依赖注入(DI)


控制反转(IoC)就是,原本代码中我们需要自己new对象,给对象赋值,自己控制对象的创建和依赖,比如



现在控制权反转给了Spring框架,Spring容器框架会帮忙new各种对象,并记录着对象之间的关系,当需要依赖一个对象的时候,直接找Spring容器,Spring容器自动注入,比如



Spring框架负责控制对象的生命周期和对象之间的关系。IoC容器在其他语言中也有应用,并非Spirng特有。IoC容器实际上就是个map(key,value),里面存放各种对象。IoC容器负责创建对象,将对象连接在一起,配置这些对象,直到它们被完全销毁。有大量的资料介绍IoC和DI的好处,比如解耦依赖,增加可扩展性。我能感受到一个比较直观的好处,比如sevice接口的实现类是ServiceImpl1,然后要改成ServiceImpl2,不用Spring这里就需要修改源码再重新编译;如果用Spring的话,修改配置文件即可,依赖注入的时候即会注入新的实现类。


2. Spring AOP


AOP(Aspect Oriented Programming 面向切面编程),最典型的应用就是通过将一些公共代码,封装成一个切面类,从而在需要使用的地方,非常方便的切入复用,大量减少重复代码。比如日志采集,权限鉴定,事务管理等。


Spring AOP的实现是基于动态代理,动态代理做了什么,即原始对象Target,执行方法do();动态代理之后得到动态代理对象Proxy,在执行方法do()的前后,会执行代理前逻辑和代理后逻辑。底层实现方式有两种:一种是 JDK 动态代理(JDK Proxy),另一种是 CGLib(Code Generation Library(基于字节码操作)) 的方式。表现形式是:



3. ContextLoaderListener


在第一部分的4中介绍了Listener,主要介绍了Tomcat负责的三大域对象相关的6个Listener。在学习Spring之后,需要再介绍一个监听器Listener,ContextLoaderListener,它是由Spring容器框架负责并提供的。在使用Spring的项目中,我们需要在web.xml中配置它,比如这样:



我们通过ContextLoaderListener的代码实现,来看看它实现了什么功能。



Spring实现了Tomcat提供的ServletContextListener接口,继承了ContextLoader,写了一个监听器ContextLoaderListener来监听Web项目启动。一旦项目启动,会触发ContextLoaderListener中的特定方法,initWebApplicationContext()方法就是用来初始化Spring的IoC容器。


ContextLoaderListener的作用就是启动Web容器时,通过web.xml中的配置,读取在contextConfigLocation中定义的xml文件,通过反射创建对象,自动装配ApplicationContext的配置信息,并产生WebApplicationContext(即IoC容器)对象,然后将这个对象放置ServletContext的属性里。这样我们就可以通过Servlet得到WebApplicationContext对象,并利用这个对象访问Spring容器管理的bean对象。简单来说,就是上面这段配置为项目提供了Spring支持,初始化了Ioc容器


4. Spring MVC


介绍Spring MVC之前,需要简单介绍一下Web开发的MVC模式。即:


  • M(Model),主要代表了逻辑处理、数据处理层(service、dao、entity);
  • V(View),主要代表了前端展示层(jsp、html);
  • C(Controller),主要代表了接受请求,调用对应的Model层,根据Model结果渲染对应的View层。


对应上Web请求就是,用户Http请求到达Servlet(Controller),然后调用相应的逻辑处理类(Model),最后把结果数据交给Jsp模版(View)渲染,最后将渲染完成的结果页面返回给用户,这样的模式就称为 MVC 模式。要说MVC的优点,就需要提一下特别早期,前后端没有分离时候的开发模式,Java代码和Html代码直接耦合,前后端开发互相依赖,都开发完才能测试。当过渡到MVC模式后,前端同学可以专注写View层,后段同学写Model层,然后通过Controller将Model层的结果数据,给到View层进行渲染。



Spring MVC是一款很优秀的 MVC 框架,是 Spring 的一个子模块,Spring 大家族中 的Web 模块,可以让我们的开发更便捷。直接从官方文档可以清晰的了解到Spring MVC框架的功能:Spring MVC框架是基于DispatcherServlet来设计的,DispatcherServlet根据可配置的映射(Mapping),将请求映射到相应的handlers(Model)类中进行逻辑处理;当在handlers中处理完获得结果数据,加上指定的逻辑view,一起传回DispatcherServlet;DispatcherServlet通过ViewResolver将逻辑view和对应的模版文件,解析成一个真正的View对象,View对象将Model中的数据渲染进来;最后返回给DispatcherServlet,再返回给请求方。尤其是现在前端通过Vie/React来开发,后台只需要提供一个json返回即可,通过Spring MVC更加简单,加上一个注解,直接将handlers处理完的结果数据,通过json形式返回给前端去渲染。


5. Interceptor


在上一部分的第5节中介绍了Filter,它依赖于Servlet容器,是实现了AOP编程思想的过滤器。作为当前最强的Java Web框架,Spring当然也提供了类似的能力,即Spring MVC中的Interceptor。Interceptor在使用方法(xml配置/注解)上和Filter是类似的,而且都利用了AOP的设计思想,不过在实现上还是有一些不同,这里主要说明一下区别


1)Filter依赖于Servlet容器,属于Servlet规范的一部分,而Interceptor依赖于Spring MVC框架;
2)Filter是由Servlet容器的函数回调(doFilter()方法)实现的,而Interceptor是通过动态代理(Java反射)实现的;
3)Filter的生命周期由Servlet容器管理,Interceptor可以通过Spring IoC容器来管理,更加方便使用;

4)Filter拦截范围在Http请求的前后,Interceptor可以在Spring的各个组件和各个方法间进行拦截加强。


6. Spring开发的两种方式XML和注解


Spring IoC容器中对象的定义,以及对象之间的依赖注入关系,可以通过XML配置和注解两种方式实现。


这里简单介绍一下Java中的注解,注解跟注释有所不同,注释就是写给人看的,而注解的目的,是可以提供给程序看的,作用有点类似于XML配置文件,比如:



怎么定义注解就不介绍了,定义了 @ComponentScan这样一个注解,并且在类/方法上使用了注解之后,怎么样生效呢?这部分工作是框架帮忙做的,框架会通过Java反射的方式,扫描出代码里出现了相关注解的类/方法,然后获取到注解里面的参数信息,然后进行逻辑处理,即通过注解的方式来减少复杂的配置文件。


Spring框架通过XML/注解的方式,定义IoC容器的对象(Bean),对象之间的DI(依赖注入)关系,方式比较灵活,组合方式多样,我这里提供一份讲解清楚的资料供大家学习。


最后主要讲一下XML方式相比注解方式最大的缺点,当项目变大之后,XML方式会导致配置复杂繁琐,并且会有很多冗余信息。所以在上手极快,开箱即用的Spring Boot中,默认就采用注解的方式进行配置,并且提供大量默认配置,极大降低了使用门槛。


三、Spring Boot


Spring Boot并不是一个新技术,而是将原来的Spring项目进行了大量的默认配置,按照“Convention over configuration”(习惯优于配置)的理念,可以不用或者仅需很少的Spring配置,就能快速创建一个能够独立运行的基于Spring框架的项目(内嵌Servlet容器的jar包)。由于非常方便,直接通过一个Spring Boot搭建Web Server的例子,直观了解一下Spring Boot。


1. Spring Boot简介


IntelliJ IDEA 14.1开始已经支持Spring Boot了,创建Spring Boot项目操作步骤如下:在File菜单里面选择 New > Project,然后选择Spring Initializr,接着一步步Next即可,只需要几秒钟,就可以构建好一个Spring Boot框架的项目。



点击完之后,就已经初始化了一个Spring Boot框架的项目了,项目结构如下:



可以看到,除了几个空目录外,包含如下几个文件:


  • pom.xml:Maven构建说明文件。
  • DemoApplication.java:一个带有main()方法的类,用于启动应用程序(关键)。
  • DemoApplicationTests.java:一个空的Junit测试类,它加载了一个使用Spring Boot字典配置功能的Spring应用程序上下文。
  • application.properties:一个空的properties配置文件,可以根据需要,添加配置属性,作用是对一些默认配置的配置值进行修改。
  • Greeting.java:一个用于Demo的POJO类。
  • GreetingController.java:一个用于处理请求的Controller类。



直接点击右上角的运行按钮,项目就启动起来了,可以看到GreetingController类中,有一个@GetMapping注解中的参数是/greeting,打开浏览器,请求http://localhost:8080/greeting,就可以从浏览器发送Http请求,通过基于Spring Boot框架的Web Server处理请求,并返回一个json字符串。



Spring Boot项目的pom.xml和普通的Spring项目的pom.xml有一些区别,首先它有一个<parent>父级依赖,在spring-boot-starter-parent中提供了很多默认的配置,这些配置可以大大简化我们的开发。通过继承spring-boot-starter-parent,默认具备了如下功能:Java版本、源码的文件编码方式(UTF-8)、依赖管理、打包支持、动态识别资源、识别插件配置、识别不同的配置(如:application-dev.properties 和 application-dev.yml),以上继承来的特性有的并非直接继承自spring-boot-starter-parent,而是继承自spring-boot-starter-parent的父级spring-boot-dependencies。并且可以通过<properties>标签覆盖parent中包含依赖包的版本,例如截图中设置了<java.version>1.8</java.version>。


Spring Boot提供了很多“开箱即用”的依赖模块,都是以spring-boot-starter-xx作为命名的。比如需要web能力,直接依赖spring-boot-starter-web,Maven会帮忙处理好Web能力背后所需的复杂依赖关系。


最后就是Spring Boot Maven插件,spring-boot-maven-plugin提供了许多方便的功能:把项目打包成一个可执行JAR(uber-JAR),包括把应用程序的所有依赖打入JAR文件内,并为JAR添加一个描述文件,其中的内容能让你用java -jar来运行应用程序;搜索public static void main()方法来标记为可运行类。



DemoApplication.java 是一个很关键的启动类,程序的入口就是这里。@SpringBootApplication是Sprnig Boot项目的核心注解,主要目的是开启自动配置。main方法就是一个标准的Java应用的main的方法,主要作用是作为项目启动的入口。此处通过对@SpringBootApplication注解和SpringApplication.run(DemoApplication.class, args)方法的底层实现,来了解Spring Boot为何能够如此方便。


1)@SpringBootApplication注解



可以看到,@SpringBootApplication注解其实是由一些注解组合而来,其中起作用的注解分别是:
  • @SpringBootConfiguration(起作用的部分是其背后的@Configuration)
  • @EnableAutoConfiguration
  • @ComponentScan


其中,@Configuration和@ComponentScan这两个注解都是第二部分的6中提到过的Spring的常用注解。


先看@Configuration,Spring IoC容器要用JavaConfig形式的配置类,需要使用@Configuration注解,Spring Boot社区推荐使用基于JavaConfig的配置形式,这里的启动类标注了@Configuration之后,它也成为了一个Spring IoC容器的配置类。


再看@ComponentScan,它对应XML配置中的<context:component-scan base-package=""/>这个标签,@ComponentScan的功能其实就是自动扫描并加载符合条件的bean文件(比如@Component、@Repository、@Service、@Controller等),最终将这些文件定义的bean加载到IoC容器中。我们可以通过basePackages等属性指定@ComponentScan扫描的文件夹范围,如果不指定,Spring框架默认从声明@ComponentScan所在类的package进行扫描。所以Spring Boot的启动类最好是放在root package下,可以默认扫描整个项目。


最后看@EnableAutoConfiguration,它到定义中有@Import(EnableAutoConfigurationImportSelector.class),借助EnableAutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助Spring Boot应用将所有需要的依赖@Configuration配置都加载到当前Spring Boot的IoC容器。



@EnableAutoConfiguration会将哪些符合条件配置类加载进来呢,通过左上角Scroll from source这个按钮,找到EnableAutoConfiguration.java的工程,然后找到META-INF/spring.factories配置文件,其中key=org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的配置类们,即为@EnableAutoConfiguration需要加载的配置类。


2)SpringApplication.run(DemoApplication.class, args) 方法


SpringApplication.run()方法做的事情比较多,比如创建一个SpringApplication对象实例,创建完之后,初始化SpringApplication对象,然后执行run方法。这个过程主要是加载各种各种预定义的配置文件,执行SpringApplicationRunListener的初始化方法,创建对应的ApplicationContext(IoC容器)并初始化等等,总的来说就是进行各种判断、各种配置文件加载、各种初始化。


四、Spring Cloud


在熟悉了Spring Boot之后,随着开发系统的规模越来越大,就会从单体架构的服务向分布式集群发展,而且随着微服务概念的兴起,微服务的治理也变得重要起来,这时候Spring Cloud这类框架就出现了。这个Spring Cloud系列教程(https://how2j.cn/k/springcloud/springcloud-intro/2035.html)作为入门级的概念了解,讲得不错,推荐给大家,阅读完耗时1-2小时。


五、结语


本文对Java Web开发技术进行了梳理,只有清楚这些概念及其之间的关系,进行工业级的Java Web应用开发才能得心应手,对于有兴趣进行底层框架(比如Trpc-Java)开发的同学,也能够更快上手。




作者简介


王展雄,腾讯大数据高级工程师,负责腾讯看点“实时/离线”数据查询系统相关研发工作,硕士毕业于复旦大学计算机科学技术学院。


注:作者所属团队「腾讯看点数据团队」,分布于深圳、上海、北京,汇聚了海内外顶级学府的优秀人才,致力于打造业界一流的数据团队,欢迎更多有识之士加入。招聘岗位:数据研发工程师、数据科学家、数据挖掘工程师等,详情参考:https://careers.tencent.com/search.html?keyword=35605


推荐阅读


超全对照!前端监控的性能指标与数据采集
主干开发、主干发布,CI 如何为大型项目提供最优解?

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存