对于Java Web中的Filter和Interceptor的理解

写在前面:因为部门项目中有用户登录验证方面的需求,故而学习了一下相关的验证技术,本文仅是作者个人学习的心得,由于水平有限,如有错误之处还请指出、见谅。

1. 背景

在设计web应用的时候,用户登录/注册是必不可少的功能,对用户登录信息进行验证的方法也是多种多样,大致可以认为如下模式:前端验证+后台验证。根据笔者的经验,一般会在前端进行一些例如是否输入数据、输入的数据的格式是否正确等一系列的验证,在后台会查询数据库进行验证。
一般在后台进行验证的时候,都会选择使用Servlet的Filter作为拦截器,本文主要介绍Servlet的Filter,然后再拿它跟Spring MVC的HnadlerInterceptor进行对比。

2. Filter

2.1 什么是Filter

Servlet作为Java Web的基础,它的一个比较核心也被广泛应用的功能就是Filter,又叫拦截器。顾名思义,拦截器就是起到拦截作用的。一般情况下,用户从客户端发出请求到服务器后,整个的流程是:

HttpRequest ----> Filter ----> Servlet ----> Controller/Action/... ----> Filter ----> HttpResponse

根据上面的流程可以看出,Filter的作用就是在用户请求到达Servlet之前,进行拦截。在拦截到用户的请求后,我们可以实现一些自定义的业务逻辑,例如之前说到的对用户登录信息进行验证。Filter还可以在服务器响应到达客户端之前对响应的数据进行修改,本文主要介绍第一个功能。

2.2 Filter的工作原理

Filter跟Servlet一样都是由服务器负责创建和销毁的,在web应用程序启动时,服务器会根据应用程序的web.xml文件中的配置信息调用public void init(FilterConfig filterConfig) throws ServletException方法来初始化Filter,在web应用程序被移除或者是服务器关闭时,会调用public void destroy()来销毁Filter。在一个应用程序中一个Filter只会被创建和销毁一次,在进行完初始化之后,Filter中声明了public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException方法,用来实现一些需要在拦截完成之后的业务逻辑。
注意到上面的doFilter()方法的参数中,有chain这个参数,它是传递过来的拦截链对象,里面包含了用户定义的一系列的拦截器,这些拦截器根据其在web.xml中定义的顺序依次被执行。当用户的信息验证通过或者当前拦截器不起作用时,我们可以执行chain.doFilter()方法来跳过当前拦截器来执行拦截器链中的下一个拦截器。

2.3 自己实现Filter

自己实现Filter时,需要继承接口javax.servlet.Filter并且实现相关的方法。

2.3.1所用到的工具:

IDE: IntelliJ IDEA
构建工具:gradle
本地服务器:Tomcat

2.3.2 具体代码

build.gradle

group 'xiangang.wei'
version '1.0-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'war'

sourceCompatibility = 1.8

repositories {   
    jcenter()    
    mavenCentral()
}
dependencies {   
    testCompile group: 'junit', name: 'junit', version: '4.11'    
    // servlet-api    
    compile group: 'javax.servlet', name: 'servlet-api', version: '2.5'
}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"               
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"           
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee        
                    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">    
<filter>        
  <filter-name>loginValidation</filter-name>      
  <filter-class>filter.LoginValidation</filter-class>
  <init-param>            
    <param-name>redirectPath</param-name>            
    <param-value>/index.jsp</param-value>        
  </init-param>        
  <init-param>            
    <param-name>disableloginValidation</param-name>            
    <param-value>N</param-value>        
  </init-param>        
  <init-param>            
    <param-name>logonString</param-name>            
    <param-value>/index.jsp</param-value>        
  </init-param>    
  </filter>    
  <filter-mapping>        
    <filter-name>loginValidation</filter-name>        
    <url-pattern>/*</url-pattern>   
   </filter-mapping>
</web-app>

web.xml中<init-param>标签被用来配置Filter的初始化时使用的参数,其中<param-name>标签表示参数的名字,可以是自己定义的任何名字,<param-value>标签表示对应的初始化参数的值。上面的初始化参数中,redirectPath定义了当验证不成功时页面重定向的的路径,logonString定义了拦截器拦截的指定URL。<filter-mapping>标签定义了拦截器的拦截模式,在<url-pattern>标签定义了拦截模式,上面的/*表示拦截所有。它和之前定义的指定拦截的URL标签结合起来使用。

index.jsp

<%--  
Created by IntelliJ IDEA.  
User: xiangang  
Date: 2016/11/21  
Time: 下午3:42  
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
 <head>    
   <title>用户登陆界面</title>   
   <style type="text/css">        
     div{            
             margin: auto;            
             border: gray 1px solid;            
             width: 70%;        
         }    
   </style>
   </head>
   <body>
     <div>    
       <form action="success.jsp" method="post">       
        <table>            
         <tr>                
           <td>用户名:<input type="text" name="name" /></td>            
         </tr>            
         <tr>                
           <td>密   码:<input type="password" name="password" /></td>            
         </tr>            
         <tr>                
           <td><input type="submit" value="提交" /></td>                
           <td><input type="reset" value="重置" /></td>            
         </tr>        
       </table>    
     </form>
   </div>
 </body>
</html>

Filter对应的实现类:
LoginValidation.java

package filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** * 
Created by xiangang on 2016/11/21. 
*/

public class LoginValidation implements Filter{    
public FilterConfig config;    
public static boolean isContains(String url, String[] regx){        
boolean flag = false;        
for (int i = 0;i<regx.length;i++){            
  if (url.indexOf(regx[i])!=-1){                
    flag = true;                
    return flag;            
  }        
}        
  return flag;    
}    

@Override    
public void init(FilterConfig filterConfig) throws ServletException {        
  this.config = filterConfig;    
}    

@Override    
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {        
  HttpServletRequest httpServletRequest= (HttpServletRequest)request;        
  HttpServletResponse httpServletResponse = (HttpServletResponse)response;        
  String name = httpServletRequest.getParameter("name");        
  String password = httpServletRequest.getParameter("password");        
  String redirectPath=httpServletRequest.getContextPath()+config.getInitParameter("redirectPath");        
  String logonString = config.getInitParameter("logonString");
  String[] logonList = logonString.split(";");    
   
  if (isContains(httpServletRequest.getRequestURI(),logonList)){            
    chain.doFilter(request,response);            
    return;        
  }
if ("Y".equals(config.getInitParameter("disableloginValidation"))){            
    chain.doFilter(request,response); 
    return;        
  }
if ("root".equals(name) && "admin".equals(password)){            
    chain.doFilter(request,response);
    return;
  }else{ 
    httpServletResponse.sendRedirect(redirectPath); 
    return; 
   } 
  }   
 
@Override    public void destroy() {
  this.config = null;    
  }
}

登录成功之后的页面:
success.jsp

<%--  
Created by IntelliJ IDEA.  
User: xiangang  
Date: 2016/11/21  
Time: 下午3:56  
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>    
<title>登录成功!</title>
</head>
<body>
欢迎!
</body>
</html>

配置好Tomcat之后,开启应用程序:

登陆界面
登录成功界面

登录失败时不会跳转,仍然会停留在原页面。

3. Interceptor

之前提到的Filter是Servlet层面的拦截器,在许多的Java Web框架中,都实现了自己的拦截器Interceptor。例如Struts2中的Interceptor、Spring MVC中的HandlerInterceptor等。相比于Filter,框架中的Interceptor的产生作用的时间和位置不一样,下面描述了应用了Spring MVC中的HandlerInterceptor的web请求流程:

HttpRequest ----> DispactherServlet ----> HandlerInterceptor ---->Controller----> HandlerInterceptor ----> HttpResponse

两者的主要区别在于Filter起作用的时机是在请求到达Servlet之前,二HandlerInterceptor其作用的时机是在DispactherServlet接收到用户请求完成请求到相应的Handler映射之后。虽然都先于在具体的业务逻辑执行,但是还是存在一些差异。Filter面对的是所有的请求,而HandlerInterceptor是面对具体的Controller。Filter总是先于HandlerInterceptor发挥作用,在Filter中甚至可以中断请求,从而使它无法到达相应的Servlet。而且两者的配置也不一样,Filter是在web.xml中进行配置,HandlerInterceptor是在具体的applicationContext.xml中进行配置。

3.1 HandlerInterceptor的工作原理

分析源码,发现HandlerInterceptor接口中声明了如下几个方法:

public interface HandlerInterceptor {  

/**    
* Intercept the execution of a handler. Called after HandlerMapping determined
* an appropriate handler object, but before HandlerAdapter invokes the handler.    
* <p>DispatcherServlet processes a handler in an execution chain, consisting    
* of any number of interceptors, with the handler itself at the end.    
* With this method, each interceptor can decide to abort the execution chain,    
* typically sending a HTTP error or writing a custom response.    
* <p><strong>Note:</strong> special considerations apply for asynchronous    
* request processing. For more details see    
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.    
* @param request current HTTP request    
* @param response current HTTP response    
* @param handler chosen handler to execute, for type and/or instance evaluation    
* @return {@code true} if the execution chain should proceed with the    
* next interceptor or the handler itself. Else, DispatcherServlet assumes    
* that this interceptor has already dealt with the response itself.    
* @throws Exception in case of errors    
*/   

boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;  
 
/**   
* Intercept the execution of a handler. Called after HandlerAdapter actually    
* invoked the handler, but before the DispatcherServlet renders the view.    
* Can expose additional model objects to the view via the given ModelAndView.    
* <p>DispatcherServlet processes a handler in an execution chain, consisting    
* of any number of interceptors, with the handler itself at the end.    
* With this method, each interceptor can post-process an execution,    
* getting applied in inverse order of the execution chain.    
* <p><strong>Note:</strong> special considerations apply for asynchronous    * request processing. For more details see    
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.    
* @param request current HTTP request    
* @param response current HTTP response    
* @param handler handler (or {@link HandlerMethod}) that started asynchronous    
* execution, for type and/or instance examination    
* @param modelAndView the {@code ModelAndView} that the handler returned    
* (can also be {@code null})    
* @throws Exception in case of errors    
*/   

void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;   

/**    
* Callback after completion of request processing, that is, after rendering    
* the view. Will be called on any outcome of handler execution, thus allows    
* for proper resource cleanup.   
* <p>Note: Will only be called if this interceptor's {@code preHandle}    
* method has successfully completed and returned {@code true}!    
* <p>As with the {@code postHandle} method, the method will be invoked on each    
* interceptor in the chain in reverse order, so the first interceptor will be    
* the last to be invoked.    
* <p><strong>Note:</strong> special considerations apply for asynchronous    
* request processing. For more details see    
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.    
* @param request current HTTP request    
* @param response current HTTP response    
* @param handler handler (or {@link HandlerMethod}) that started asynchronous    
* execution, for type and/or instance examination    
* @param ex exception thrown on handler execution, if any    
* @throws Exception in case of errors    
*/   

void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;

}

这三个方法分别会在具体的HandlerController方法执行之前,执行成功之后,和执行完成之后被执行。

3.2 自己实现HandlerInterceptor

Spring MVC框架采用了适配器的开发模式,使用一个抽象的类HandlerInterceptorAdapter实现HandlerInterceptor接口,这样当我们需要自己实现HandlerInterceptor时,我们可以继承HandlerInterceptorAdapter这样我们就不用全部实现这三个方法,而可以选择性的实现自己需要的方法。

3.2.1所用到的工具:

IDE: IntelliJ IDEA
构建工具:gradle
本地服务器:Tomcat

3.2.2具体的代码:

build.gradle

group 'xiangang.wei'
version '1.0-SNAPSHOT'
apply plugin: 'java'
apply plugin: 'war'source

Compatibility = 1.8

repositories {    
jcenter()    
mavenCentral()
}

dependencies {    
testCompile group: 'junit', name: 'junit', version: '4.11'    
// servlet-api    
compile group: 'javax.servlet', name: 'servlet-api', version: '2.5'    
//spring相关    
compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.3.RELEASE'    
compile group: 'org.springframework', name: 'spring-orm', version: '4.3.3.RELEASE'    
compile group: 'org.springframework', name: 'spring-aspects', version: '4.3.3.RELEASE'    
compile group: 'org.springframework.security', name: 'spring-security-config', version: '3.2.0.RELEASE'    
compile group: 'org.springframework.security', name: 'spring-security-taglibs', version: '3.2.0.RELEASE'    
compile 'org.springframework.security:spring-security-web:3.2.0.RELEASE'    //hibernate相关    
compile 'org.hibernate:hibernate-core:4.3.6.Final'    
//mysql    
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.39'    
//springData    
compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.10.3.RELEASE'    
// https://mvnrepository.com/artifact/log4j/log4j日志    
compile group: 'log4j', name: 'log4j', version: '1.2.17'   
//json解析相关    
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.5.4'    
compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.5.4'
}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"           
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"           
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee        
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"           version="3.0">    
<!--框架默认帮我们配置好了ApplicationContext的实现类(org.springframework.web.context.support.XmlWebApplicationContext),不需要自己手动配置-->    

<!--配置ApplicationContext需要加载的配置文件-->    
<context-param>        
<param-name>contextConfigLocation</param-name>       
<param-value>classpath:databaseAccess.xml,classpath:service.xml</param-value>    
</context-param>    

<!--ApplicationContext的加载和关闭-->    
<listener>        
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>    
</listener>    
<!--配置字符过滤器,防止出现中文乱码-->    
<filter>        
<filter-name>CharacterEncodingFilter</filter-name>        
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>        
<init-param>            
<param-name>encoding</param-name>            
<param-value>utf-8</param-value>        
</init-param>    
</filter>    
<filter-mapping>        
<filter-name>CharacterEncodingFilter</filter-name>        
<url-pattern>/*</url-pattern>    
</filter-mapping>    

<!--配置Spring MVC的前置控制器-->    
<servlet>        
<servlet-name>dispatcherServlet</servlet-name>        
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>        
<init-param>            
<param-name>contextConfigLocation</param-name>            
<param-value>classpath:dispatcherServlet.xml</param-value>        
</init-param>        
<load-on-startup>1</load-on-startup>    
</servlet>    
<servlet-mapping>        
<servlet-name>dispatcherServlet</servlet-name>        
<url-pattern>/</url-pattern>    
</servlet-mapping>
</web-app>

涉及到HandlerInterceptor的配置文件dispatcherServlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"       
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       
xmlns:context="http://www.springframework.org/schema/context"       
xmlns:mvc="http://www.springframework.org/schema/mvc"       
xsi:schemaLocation="http://www.springframework.org/schema/beans       
http://www.springframework.org/schema/beans/spring-beans.xsd       
http://www.springframework.org/schema/context       
http://www.springframework.org/schema/context/spring-context.xsd       
http://www.springframework.org/schema/mvc       
http://www.springframework.org/schema/mvc/spring-mvc.xsd">    

<!--开启注解-->    
<mvc:annotation-driven/>    

<!--添加需要扫描的包-->    
<context:component-scan base-package="ims" use-default-filters="false">        
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>    
</context:component-scan>    

<!--添加HandlerInterceptor-->    
<mvc:interceptors>        
<mvc:interceptor>            
<mvc:mapping path="/user/**"/>            
<bean class="ims.handlerInterceptor.LoginInterceptor"/>        
</mvc:interceptor>        
<mvc:interceptor>            
<mvc:mapping path="/register/**"/>            
<bean class="ims.handlerInterceptor.RegisterInterceptor"/>        
</mvc:interceptor>    
</mvc:interceptors>    

<!--添加试图解析器-->    
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">        
<property name="prefix" value="/WEB-INF/views/"/>        
<property name="suffix" value=".jsp"/>    
</bean>

</beans>

配置HandlerInterceptor有两种方式,一种是如上面所示,这张方式配置的HandlerInterceptor可以指定具体的拦截路径,另外一种方式是直接在<mvc:interceptors>中使用<bean>标签进行配置:<bean class="ims.handlerInterceptor.LoginInterceptor"/> 按照这种方式配置的HandlerInterceptor会对所有的路径进行拦截。

具体的Interceptor实现类:
LoginInterceptor.java

package ims.handlerInterceptor;
import ims.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/** 
* Created by xiangang on 16/11/17. 
*/

public class LoginInterceptor extends HandlerInterceptorAdapter {    

@Autowired    
private UserService userService;   
 
@Override    
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        
  boolean flag = true;        
  HttpSession session = request.getSession();        
  if (session.getAttribute("user") == null) {           
  String userName = request.getParameter("userName");            
  String password = request.getParameter("password");            
  flag = userService.selectByUserName(userName, password);            
  if (flag){                
    session.setAttribute("user",userName);            
    }        
  }        
  if (!flag){            
    response.sendRedirect("index.jsp");        
    }        
  return flag;   
  }
}

后续的其他代码这里就不一一贴出相关代码在:https://github.com/xiangang-wei/Inteceptor

具体的运行结果这里也就不再贴图了,实现的效果跟之前的Filter是一致的。

3. 总结

本文主要是对自己在项目中使用的相关技术进行总结,关于这个知识点还有很多内容没有覆盖到,读者如有兴趣,可以查看相关源码进行分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容

  • 本文包括:1、Filter简介2、Filter是如何实现拦截的?3、Filter开发入门4、Filter的生命周期...
    廖少少阅读 7,272评论 3 56
  • 拦截一些请求进行处理,比如通过它来进行权限验证,或者是来判断用户是否登陆,日志记录,编码,或者限制时间点访问等等,...
    JackFrost_fuzhu阅读 1,979评论 0 7
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,656评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,246评论 11 349
  • 1. 今年北京文科状元说了一番话。他说,现在往往都是家庭条件好的孩子,才能在高考中胜出,因为他们见识广啊。 2. ...
    快刀笔吏阅读 670评论 0 0