看完这篇总结,你会发现其实spring面试真的没那么难,一篇帮你彻底搞定spring。

Posted 前程有光

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了看完这篇总结,你会发现其实spring面试真的没那么难,一篇帮你彻底搞定spring。相关的知识,希望对你有一定的参考价值。

什么是Spring 框架?Spring 框架有哪些主要模块?

Spring框架是一个为Java应用程序开发提供了综合、广泛的基础性支持的平台。Spring集成了Java开发过程中所需的各种组件、库和依赖包,使得开发人员可以专注于应用的开发,而不用在各种底层组件依赖上花费大量时间,并且这些时间在每个项目上是重复浪费的。

Spring 框架已经集成了20 多个模块。这些模块主要包含核心容器(spring-core/spring-beans)、数据访问/集成、Web(spring-web)、AOP(spring-aop/spring-aspects)、工具、消息和测试模块。

另外这里个人整理了一些资料,有需要的朋友可以直接点击领取。

Java基础知识合集

百本Java架构师核心书籍

对标阿里P7的java架构师学习路线图

结合金三银四整理的2021年最新面试题

使用Spring 框架能带来哪些好处?

Spring 框架带来的主要好处:

  • 与EJB容器相比较,IOC 容器更加趋向于轻量级,使得IOC 容器可以在有限的内存和CPU 资源的情况下进行应用程序的开发和发布。

  • DI机制降低了业务对象替换的复杂性,提高了组件之间的解耦。

  • Spring的AOP支持允许将一些通用任务如安全、事务、日志等进行集中式管理,从而提供了更好的复用。

  • 除了自身已有的技术外(比如ORM 框架、logging 框架、J2EE、Quartz和JDK Timer),Spring还集成了很多其他基础技术,比如JDBC、Transaction等。

  • Spring 框架是按照模块的形式来组织的。由包和类的编号就可以看出其所属的模块,Spring并不强制应用完全依赖于Spring,开发者可自由选用Spring框架的部分或全部。

  • Spring的ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问。

什么是IOC?什么是DI?

  • IOC(Inversion of Control)叫控制反转, DI(Dependency Injection)叫依赖注入,是对IOC更简单的诠释。

  • IOC 控制反转是把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的"控制反转"就是对组件对象控制权的转移,从程序代码本身转移到了外部容器,由容器来创建对象并管理对象之间的依赖关系。

  • DI 依赖注入的基本原则是应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由容器负责,查找资源的逻辑应该从应用组件的代码中抽取出来,交给容器来完成。DI是对IOC更准确的描述,即组件之间的依赖关系由容器在运行期决定,形象的来说,即由容器动态的将某种依赖关系注入到组件之中。

在Java中依赖注入有哪些方式?

  • 接口注入:指的就是在接口中定义要注入的信息,并通过接口完成注入。

  • set注入:指的就是在接受注入的类中定义一个set方法,并在参数中定义需要注入的元素。

  • 构造器注入:指的就是接受注入的类中定义一个构造方法,并在参数中定义需要注入的元素。

@Component("visitor")
 public class VisitTeacher {
 
     // 接口注入
     @Autowired
     private Teacher teacher;
     
     // 构造器注入
     @Autowired
     public VisitTeacher(Teacher teacher) {
         this.teacher = teacher;
     }
     
     // Setter注入
     @Autowired
     public void setTeacher(Teacher teacher) {
         this.teacher = teacher;
     }
     
     public void say() {
         teacher.say();
     }
 }
  • 设值注入:先通过无参数的构造函数创建一个Bean实例,然后调用对应的setter方法注入依赖关系(设置注入就是指要被注入的类中定义有一个setter()方法,并在参数中定义需要注入的对象。)

  • 构造注入:直接调用有参数的构造器,当bean实例创建完成后,已经完成了依赖关系的注入(构造注入就是指要被注入的类中声明一个构造方法,并在此方法的参数中定义要注入的对象。)

BeanFactory 和ApplicationContext 有什么区别?

BeanFactory 可以理解为含有bean 集合的工厂类。BeanFactory 包含了种bean 的定义,以便在接收到客户端请求时将对应的bean 实例化。BeanFactory 还能在实例化对象时生成协作类之间的关系。BeanFactory 还包含了bean 生命周期的控制,调用客户端的初始化方法(initialization Methods)和销毁方法(destruction Methods)。

从表面上看,ApplicationContext 如同bean factory 一样具有bean 定义、bean 关联关系的设置,根据请求分发bean的功能。但ApplicationContext在此基础上还提供了其他的功能。

  • 提供了支持国际化的文本消息

  • 统一的资源文件读取方式

  • 已在监听器中注册的bean 的事件

以下是三种较常见的ApplicationContext 实现方式:

  • ClassPathXmlApplicationContext:从classpath 的XML 配置文件中读取上下文,并生成上下文定义。应用程序上下文从程序环境变量中取得。

ApplicationContext context = new ClassPathXmlApplicationContext(“application.xml”);

  • FileSystemXmlApplicationContext :由文件系统中的XML 配置文件读取上下文。

ApplicationContext context = new FileSystemXmlApplicationContext(“application.xml”);

  • XmlWebApplicationContext:由Web 应用的XML 文件读取上下文。

Spring 提供几种配置方式来设置元数据?

将Spring 配置到应用开发中有以下三种方式:

  • 基于XML 的配置

  • 基于注解的配置

  • 基于Java 的配置

前两种比较简单,主要介绍基于Java Config的配置化。Spring3.0提供了一些注解支持通过java类定义spring配置元数据,可直接消除xml配置文件。例如:

  • @Configuration

  • @Bean

  • @DependsOn

  • @Primary

  • @Lazy

  • @Import

  • @ImportResource

  • @Value

如何使用XML 配置的方式配置Spring?

SpringXML 配置的主要目的时候是使所有的Spring 组件都可以用xml 文件的形式来进行配置。这意味着不会出现其他的Spring 配置类型(比如声明的方式或基于Java Class 的配置方式)。

Spring 的XML 配置方式是使用被Spring 命名空间的所支持的一系列的XML 标签来实现的。Spring有以下主要的命名空间:context、beans、jdbc、tx、aop、mvc 和aso。

 <beans>
     <!-- JSON Support -->
     <bean name="viewResolver"
         class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
     <bean name="jsonTemplate"
         class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
     <bean id="restTemplate" class="org.springframework.web.client.RestTemplate"/>
 </beans>

下面这个web.xml 仅仅配置了DispatcherServlet,这件最简单的配置便能满足应用程序配置运行时组件的需求。

 <web-app>
     <display-name>Archetype Created Web Application</display-name>
     <servlet>
         <servlet-name>spring</servlet-name>
         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
         <load-on-startup>1</load-on-startup>
     </servlet>
     <servlet-mapping>
         <servlet-name>spring</servlet-name>
         <url-pattern>/</url-pattern>
     </servlet-mapping>
 </web-app>

Spring 提供哪些配置形式?

根据[Pt6]的描述,Spring提供了3种配置形式。

Spring对Java配置的支持是由@Configuration注解和@Bean注解来实现的。由@Bean注解的方法将会实例化、配置和初始化一个新对象,这个对象将由Spring的IOC容器来管理。@Bean声明所起到的作用与XML元素类似。被@Configuration 所注解的类则表示这个类的主要目的是作为bean 定义的资源。被@Configuration声明的类可以通过在同一个类的内部调用@bean方法来设置嵌入bean 的依赖关系。

 // 最简单的@Configuration 声明类请参考下面的代码:
 @Configuration
 public class AppConfig{
     @Bean
     public MyService myService() {
         return new MyServiceImpl();
     }
 }

对于上面的@Beans 配置文件相同的XML 配置文件如下:

 <beans>
     <bean id="myService" class="com.demo.services.MyServiceImpl"/>
 </beans>

上述配置方式的实例化方式如下:利用AnnotationConfigApplicationContext 类进行实例化

 public static void main(String[] args) {
     ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
     MyService myService = ctx.getBean(MyService.class);
     myService.doStuff();
 }

要使用组件组建扫描,仅需用@Configuration 进行注解即可:

 @Configuration
 @ComponentScan(basePackages = "com.demo")
 public class AppConfig {
 }

在上面的例子中,com.demo 包首先会被扫到,然后再容器内查找被@Component 声明的类,找到后将这些类按照Spring bean 定义进行注册。

怎样用注解的方式配置Spring?

Spring 在2.5 版本以后开始支持用注解的方式来配置依赖注入。

可以用注解的方式来替代XML方式的bean 描述,可以将bean 描述转移到组件类的内部,只需要在相关类上、方法上或者字段声明上使用注解即可。

注解注入将会被容器在XML 注入之前被处理,所以后者会覆盖掉前者对于同一个属性的处理结果。注解装配在Spring 中是默认关闭的。所以需要在Spring 文件中配置一下才能使用基于注解的装配模式。

如果你想要在你的应用程序中使用关于注解的方法的话,请参考如下的配置。

 <beans>
     context:annotation-config/
 </beans>

在标签配置完成以后,就可以用注解的方式在Spring 中向属性、方法和构造方法中自动装配变量。

下面是几种比较重要的注解类型:

  • @Required:该注解应用于设值方法。

  • @Autowired:该注解应用于有值设值方法、非设值方法、构造方法和变量。

  • @Qualifier:该注解和@Autowired 注解搭配使用,用于消除特定bean 自动装配的歧义。

  • JSR-250 Annotations:Spring 支持基于JSR-250 注解的以下注解,@Resource、@PostConstruct和@PreDestroy。

请解释Spring Bean 的生命周期?

在一个bean 实例被初始化时,需要执行一系列的初始化操作以达到可用的状态。同样的,当一个bean 不在被调用时需要进行相关的析构操作,并从bean 容器中移除。SpringBeanFactory 负责管理在spring 容器中被创建的bean 的生命周期。Bean 的生命周期由两组回调(call back)方法组成。

  • 初始化之后调用的回调方法。

  • 销毁之前调用的回调方法。

Spring 框架提供了以下四种方式来管理bean 的生命周期事件:

  • InitializingBean 和DisposableBean 回调接口

  • 针对特殊行为的其他Aware 接口

  • Bean 配置文件中的Custom init()方法和destroy()方法

  • @PostConstruct 和@PreDestroy 注解方式

使用customInit()和customDestroy()方法管理bean 生命周期的代码样例如下:

````
 
 
 

 

 

#  Spring Bean 作用域之间的区别?
Spring 容器中的bean 可以分为5 个范围。

*  singleton:这种bean 范围是默认的,这种范围确保不管接受到多少个请求,每个容器中只有一个bean 的实例,单例的模式由bean factory 自身来维护。

*  prototype:原形范围与单例范围相反,为每一个bean 请求提供一个实例。

*  request:在请求bean 范围内会每一个来自客户端的网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。

*  Session:与请求范围类似,确保每个session 中有一个bean 的实例,在session 过期后,bean 会随之失效。

*  global-session:global-session 和Portlet 应用相关。当你的应用部署在Portlet 容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet 共用全局的存储变量的话,那么这全局变量需要存储在global-session 中。全局作用域与Servlet 中的session 作用域效果相同。

 

# 什么是Spring inner beans?
在Spring 框架中,无论何时bean 被使用时,当仅被调用了一个属性(作为POJO类使用,仅提供属性的set/get能力,没有其它行为)。一个明智的做法是将这个bean声明为内部bean。内部bean 可以用setter 注入“属性”和构造方法注入“构造参数”的方式来实现。

比如,在我们的应用程序中,一个Customer 类引用了一个Person 类,我们的要做的是创建一个Person的实例,然后在Customer 内部使用

public class Customer{
     private Person person;
 
     //Setters and Getters
 
 }
 public class Person{
     private String name;
     private String address;
     private int age;
 
     //Setters and Getters
 }
 
     
         
         
             
             
             
         

     

 

 

# Spring 框架中的单例Beans 是线程安全的么?
Spring 框架并没有对单例bean 进行任何多线程的封装处理。关于单例bean 的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean 并没有可变的状态(比如Serview 类和DAO类),所以在某种程度上说Spring 的单例bean 是线程安全的。如果你的bean 有多种状态的话(比如View Model 对象),就需要自行保证线程安全。

最浅显的解决办法就是将多态bean 的作用域由“singleton”变更为“prototype”。

 

# 请举例说明如何在Spring 中注入一个Java 集合?
Spring 提供了以下四种集合类的配置元素:

*  <list>标签用来装配可重复的list 值。

*  <set>标签用来装配没有重复的set 值。

*  ‘<map>该标签可用来注入键和值可以为任何类型的键值对。

*  <props>该标签支持注入键和值都是字符串类型的键值对。

 

### 下面看一下具体的例子:


 
 
 
  INDIA
  Pakistan
  USA
  UK
 

 

 
 
   
   
    INDIA
    Pakistan
    USA
    UK
   

   

   
   
   
   
   
   
   
   

   

   
   
   
    admin@gupaoedu.com
    support@gupaoedu.com
   

   

     

 

# 如何向Spring Bean 中注入java.util.Properties?
使用如下面代码所示的标签:


 
 
  admin@gupaoedu.com
  support@gupaoedu.com
 

 

 


#  请解释Spring Bean 的自动装配?
在Spring 框架中,在配置文件中设定bean 的依赖关系是一个很好的机制,Spring 容器还可以自动装配合作关系bean 之间的关联关系。这意味着Spring 可以通过向Bean Factory 中注入的方式自动搞定bean 之间的依赖关系。自动装配可以设置在每个bean 上,也可以设定在特定的bean 上。

下面的XML 配置文件表明了如何根据名称将一个bean 设置为自动装配:

 <bean id="employeeDAO" class="com.gupaoedu.EmployeeDAOImpl" autowire="byName" />
 

除了bean配置文件中提供的自动装配模式,还可以使用@Autowired注解来自动装配指定的bean。在使用@Autowired注解之前需要在按照如下的配置方式在Spring 配置文件进行配置才可以使用。

 <context:annotation-config />
 

也可以通过在配置文件中配置AutowiredAnnotationBeanPostProcessor 达到相同的效果。

 <bean class ="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
 

配置好以后就可以使用@Autowired 来标注了。

@Autowired
 public EmployeeDAOImpl ( EmployeeManager manager ) {
  this.manager = manager;
 }

 

# 自动装配有哪些局限性?
自动装配有如下局限性:

*  重写:你仍然需要使用<property>设置指明依赖,这意味着总要重写自动装配。

*  原生数据类型:你不能自动装配简单的属性,如原生类型、字符串和类。

*  模糊特性:自动装配总是没有自定义装配精确,因此,如果可能尽量使用自定义装配。

 

# 请解释各种自动装配模式的区别?
在Spring 框架中共有5 种自动装配,让我们逐一分析。

*  no:这是Spring 框架的默认设置,在该设置下自动装配是关闭的,开发者需要自行在bean 定义中用标签明确的设置依赖关系。

*  byName:该选项可以根据bean名称设置依赖关系。当向一个bean中自动装配一个属性时,容器将根据bean的名称自动在在配置文件中查询一个匹配的bean。如果找到的话,就装配这个属性,如果没找到的话就报错。

*  byType:该选项可以根据bean类型设置依赖关系。当向一个bean中自动装配一个属性时,容器将根据bean的类型自动在在配置文件中查询一个匹配的bean。如果找到的话,就装配这个属性,如果没找到的话就报错。

*  constructor:构造器的自动装配和byType模式类似,但是仅仅适用于与有构造器相同参数的bean,如果在容器中没有找到与构造器参数类型一致的bean,那么将会抛出异常。

*  autodetect:该模式自动探测使用构造器自动装配或者byType自动装配。首先,首先会尝试找合适的带参数的构造器,如果找到的话就是用构造器自动装配,如果在bean 内部没有找到相应的构造器或者是无参构造器,容器就会自动选择byTpe 的自动装配方式。

 

# 请举例解释@Required?
@Required 注释应用于 bean 属性的 setter 方法,它表明受影响的 bean 属性在配置时必须放在 XML 配置文件中,否则容器就会抛出一个 BeanInitializationException 异常。下面显示的是一个使用 @Required 注释的示例。

// 使用Spring IoC实例化对象时,如果age和name未设置,将会报BeanInitializationException错误
 public class Student {
    private Integer age;
    private String name;
    @Required
    public void setAge(Integer age) {
       this.age = age;
    }
    public Integer getAge() {
       return age;
    }
    @Required
    public void setName(String name) {
       this.name = name;
    }
    public String getName() {
       return name;
    }
 }


# 请举例说明@Qualifier?
可能会有这样一种情况,当你创建多个具有相同类型的 bean时,并且想要用一个属性只为它们其中的一个进行装配,在这种情况下,你可以使用@Qualifier 注释和 @Autowired 注释通过指定哪一个真正的 bean 将会被装配来消除混乱。@Qualifier注解意味着可以在被标注bean的字段上按照名称进行自动装配。

public class Profile {
    @Autowired
    @Qualifier("student1")
    private Student student;
    public Profile(){
       System.out.println("Inside Profile constructor." );
    }
    public void printAge() {
       System.out.println("Age : " + student.getAge() );
    }
    public void printName() {
       System.out.println("Name : " + student.getName() );
    }
 }


# 构造方法注入和设值注入有什么区别?
### 相比而言设值(Setter)注入具有以下优点:

*  与传统的JavaBean的写法更相似,程序开发人员更容易理解、接受。通过setter方法设定依赖关系显得更加直观、自然。

*  对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿,难以阅读。Spring在创建Bean实例时,需要同时实例化其依赖的全部实例,因而导致性能下降。而是用设置注入可以避免这些问题。

*  尤其在某些属性可选的情况下,多参数的构造器更加笨重。

 

### 某些情况下,构造注入的优势:

*  构造注入可以再构造器中决定依赖关系的注入顺序,有限依赖的优先注入。 例如,组件中其它依赖关系的注入,常常需要依赖于Datasource的注入。 采用构造注入,可以在代码中清晰地决定注入顺序。

*  对于依赖关系无需变化的Bean,构造注入更加有用。因为没有setter方法,所有的依赖关系全部在构造器内设定。因此,无需担心后续代码对依赖关系的破坏。

*  依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。 建议:采用设值注入为主,构造注入为辅的注入策略。对于依赖关系无需变化的注入,尽量采用构造注入; 而其它的依赖关系的注入,则考虑设值注入。

 

# Spring 框架中有哪些不同类型的事件?
Spring 的ApplicationContext 提供了支持事件和代码监听器的功能。

我们可以创建bean 用来监听在ApplicationContext 中发布的事件。ApplicationEvent 类在ApplicationContext 接口中处理的事件,如果一个bean 实现了ApplicationListener 接口,当一个ApplicationEvent 被发布以后,bean 会自动被通知。

public class AllApplicationEventListener implements ApplicationListener {
     @Override
     public void onApplicationEvent(ApplicationEvent applicationEvent) {
    //process event
    }
 }

 

### Spring 提供了以下5 中标准的事件:

*  上下文更新事件(ContextRefreshedEvent):该事件会在ApplicationContext 被初始化或者更新时发布。也可以在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。

*  上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext 的Start()方法开始/重新开始容器时触发该事件。

*  上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext 的Stop()方法停止容器时触发该事件。

*  上下文关闭事件(ContextClosedEvent):当ApplicationContext 被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean 都被销毁。

*  请求处理事件(RequestHandledEvent):在Web 应用中,当一个http 请求(request)结束触发该事件。

 

除了上面介绍的事件以外,还可以通过扩展ApplicationEvent 类来开发自定义的事件。

public class CustomApplicationEvent extends ApplicationEvent {
     public CustomApplicationEvent ( Object source, final String msg ){
         super(source);
         System.out.println("Created a Custom event");
    }
 }

为了监听这个事件,还需要创建一个监听器:

public class CustomEventListener implements ApplicationListener < CustomApplicationEvent >{
     @Override
     public void onApplicationEvent(CustomApplicationEvent applicationEvent) {
    }
 }

之后通过applicationContext 接口的publishEvent()方法来发布自定义事件。

CustomApplicationEvent customEvent = new CustomApplicationEvent(applicationContext,“Test message”);
 applicationContext.publishEvent(customEvent);

# FileSystemResource 和ClassPathResource 有何区别?
在FileSystemResource 中需要给出spring-config.xml 文件在你项目中的相对路径或者绝对路径。

在ClassPathResource 中spring 会在ClassPath 中自动搜寻配置文件,所以要把ClassPathResource 文件放在ClassPath 下。如果将spring-config.xml 保存在了src 文件夹下的话,只需给出配置文件的名称即可,因为src 文件夹是默认。

简而言之,ClassPathResource 在环境变量中读取配置文件,FileSystemResource 在配置文件中读取配置文件。

 

# Spring 框架中都用到了哪些设计模式?
Spring 框架中使用到了大量的设计模式,下面列举了比较有代表性的:

*  代理模式:在AOP 和remoting 中被用的比较多。

*  单例模式:在spring 配置文件中定义的bean 默认为单例模式。

*  模板模式:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。

*  委派模式:Srping 提供了DispatcherServlet 来对请求进行分发。

*  工厂模式:BeanFactory 用来创建对象的实例,贯穿于BeanFactory / ApplicationContext 接口的核心理念。

*  代理模式:AOP 思想的底层实现技术,Spring 中采用JDK Proxy 和CgLib 类库。

 

# 在Spring 框架中如何更有效的使用JDBC?
使用Spring JDBC 框架,资源管理以及错误处理的代价都会减轻。开发人员只需通过statements 和queries 语句从数据库中存取数据。Spring框架中通过使用模板类能更有效的使用JDBC,也就是所谓的JdbcTemplate。

 

# 请解释下Spring框架中的IOC容器?
*  Spring中的 org.springframework.beans 包和 org.springframework.context包构成了Spring框架IoC容器的基础。

*  BeanFactory接口提供了一个先进的配置机制,使得任何类型的对象的配置成为可能。

*  ApplicationContex接口对BeanFactory(是一个子接口)进行了扩展,在BeanFactory的基础上添加了其他功能,比如与Spring的AOP更容易集成,也提供了处理messageresource的机制(用于国际化)、事件传播以及应用层的特别配置,比如针对Web应用的WebApplicationContext。

*  org.springframework.beans.factory.BeanFactory 是Spring IoC容器的具体实现,用来包装和管理前面提到的各种bean。BeanFactory接口是Spring IoC容器的核心接口。

 

# 在Spring 中可以注入null 或空字符串吗?
完全可以。

<null/>用于处理null值。Spring会把属性的空参数当作空字符串处理。以下的xml片断将email属性设为空字符串。


   
 


这等同于Java代码: exampleBean.setEmail("")。而null值则可以使用<null>元素可用来表示。例如:


   
 

上述的配置等同于Java代码:exampleBean.setEmail(null)。
# 最后
都看到这里了,各位大佬不妨点个赞再走呗!

以上是关于看完这篇总结,你会发现其实spring面试真的没那么难,一篇帮你彻底搞定spring。的主要内容,如果未能解决你的问题,请参考以下文章

2021最新阿里Java高级面试题总结,看完这篇彻底明白了

面试还在被红黑树虐?看完这篇轻松搞定面试官

看完这篇Bean的作用域与生命周期,问到面试官不敢问--乐字节java

看完这篇Bean的作用域与生命周期,问到面试官不敢问--乐字节java

如何在二三线城市月薪过万面试100人后的经验总结!看完这篇让你至少涨薪2000

如何在二三线城市月薪过万面试100人后的经验总结!看完这篇让你至少涨薪2000