Java程序员面试题集(151-180)
Java面试题集(151-180)
摘要:这部分包含了Spring、Spring MVC以及Spring和其他框架整合以及测试相关的内容,除此之外还包含了大型网站技术架构相关面试内容。
151. Spring中的BeanFactory和ApplicationContext有什么联系?
答:Spring通过配置文件描述Bean以及Bean之间的依赖关系,利用Java的反射机制实现Bean的实例化,并建立Bean之间的依赖关系,在此基础上,Spring的IoC容器还提供了Bean实例缓存、生命周期管理、Bean实例代理、事件发布、资源装载等高级服务。BeanFactory是Spring框架最核心的接口,它提供了IoC容器的配置机制。ApplicationContext建立在BeanFactory之上,提供了更多面向应用的功能,包括对国际化和框架事件体系的支持。通常将BeanFactory称为IoC容器,而ApplicationContext称为应用上下文,前者更倾向于Spring本身,后者更倾向于开发者,因此被使用得更多。
【补充】反射(reflection)又叫自省(introspection),是获得对象或类型元数据的方法,Java反射机制可以在运行时判断对象所属的类,在运行时构造任意一个类的对象,在运行时获得一个类的属性和方法,在运行时调用对象的方法,或者生成动态代理。在Java中,可以通过类的Class对象获得类的构造器、属性、方法等类的元数据,还可以访问这些属性或调用这些方法,和反射相关的类还包括:
- Constructor:代表类的构造器的类。通过Class对象的getConstructors方法可以获得类的所有构造器的数组,Java 5以后的版本还可以通过getConstrcutor(Class... parameterTypes)获得拥有特定参数的构造器对象。Constructor对象的一个主要方法是newInstance,通过该方法可以创建一个类的实例。
- Method:代表方法的类。通过Class对象的getDeclaredMethods方法可以获得所有方法的数组,Java 5以后的版本还可以通过getDeclaredMethod(String name, Class... parameterTypes)获得特定签名的方法,其中name是方法名,可变参数代表方法的参数列表。Method对象最重要的方法是invoke(Object obj, Object[] args),其中obj是调用该方法的目标对象,args是传给方法的参数,这样就可以调用指定对象的方法。此外,Method对象还包括以下重要方法:
- Class getReturnType():获取方法的返回类型。
- Class[] getParameterTypes():获取方法参数类型的数组。
- Class[] getExceptionTypes():获取方法异常类型数组。
- Annotation[][] getParameterAnnotations():获得方法参数注解信息的数组。
- Field:代表属性的类。通过Class对象的getDeclaredFields()方法可以获取类的属性数组。通过getDeclaredField(String name)则可以获取某个特定名称的属性。Field对象最重要的方法是set(Object obj, Object value),其中obj是目标对象,而value是要赋给属性的值。
除此之外,Java还提供了Package类用于包的反射,Java 5以后的版本还提供了AnnotationElement类用于注解的反射。总之,Java的反射机制保证了可以通过编程的方式访问目标类或对象的所有元素,对于被private和protected访问修饰符修饰的成员,只要JVM的安全机制允许,也可以通过反射进行调用。下面是一个反射的例子:
Car.java
- package com.lovo;
- public class Car {
- private String brand;
- private int currentSpeed;
- private int maxSpeed;
- public Car(String brand, int maxSpeed) {
- this.brand = brand;
- this.maxSpeed = maxSpeed;
- }
- public void run() {
- currentSpeed += 10;
- System.out.println(brand + " is running at " + currentSpeed + " km/h");
- if(currentSpeed > maxSpeed) {
- System.out.println("It's dangerous!");
- }
- }
- }
- package com.lovo;
- import java.lang.reflect.Constructor;
- import java.lang.reflect.Field;
- import java.lang.reflect.Method;
- public class CarTest {
- public static void main(String[] args) throws Exception {
- Constructor<Car> con = Car.class.getConstructor(String.class, int.class);
- Car myCar = (Car) con.newInstance("Benz", 280);
- Method m = myCar.getClass().getDeclaredMethod("run");
- for(int i = 0; i < 10; i++) {
- m.invoke(myCar);
- }
- Field f1 = myCar.getClass().getDeclaredField("maxSpeed");
- Field f2 = myCar.getClass().getDeclaredField("brand");
- f1.setAccessible(true);
- f1.set(myCar, 80);
- f2.setAccessible(true);
- f2.set(myCar, "QQ");
- m.invoke(myCar);
- }
- }
152. Spring中Bean的作用域有哪些?
答:在Spring的早期版本中,仅有两个作用域:singleton和prototype,前者表示Bean以单例的方式存在;后者表示每次从容器中调用Bean时,都会返回一个新的实例,prototype通常翻译为原型,而设计模式中的创建型模式中也有一个原型模式,原型模式也是一个常用的模式,例如做一个室内设计软件,所有的素材都在工具箱中,而每次从工具箱中取出的都是素材对象的一个原型,可以通过对象克隆来实现原型模式。Spring 2.x中针对WebApplicationContext新增了3个作用域,分别是:request(每次HTTP请求都会创建一个新的Bean)、session(同一个HttpSession共享同一个Bean,不同的HttpSession使用不同的Bean)和globalSession(同一个全局Session共享一个Bean)。
需要指出的是:单例模式和原型模式都是重要的设计模式。一般情况下,无状态或状态不可变的类适合使用单例模式。在传统开发中,由于DAO持有Connection这个非线程安全对象因而没有使用单例模式;但在Spring环境下,所有DAO类对可以采用单例模式,因为Spring利用AOP和Java API中的ThreadLocal对非线程安全的对象进行了特殊处理。
【补充】ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。ThreadLocal,顾名思义是线程的一个本地化对象,当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。
ThreadLocal类非常简单好用,只有四个方法,能用上的也就是下面三个方法:
- void set(T value):设置当前线程的线程局部变量的值。
- T get():获得当前线程所对应的线程局部变量的值。
- void remove():删除当前线程中线程局部变量的值。
- package com.lovo;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.Map;
- public class MyThreadLocal<T> {
- private Map<Thread, T> map = Collections.synchronizedMap(new HashMap<Thread, T>());
- public void set(T newValue) {
- map.put(Thread.currentThread(), newValue);
- }
- public T get() {
- return map.get(Thread.currentThread());
- }
- public void remove() {
- map.remove(Thread.currentThread());
- }
- }
153. 你如何理解AOP中的连接点(Joinpoint)、切点(Pointcut)、增强(Advice)、引介(Introduction)、织入(Weaving)、切面(Aspect)这些概念?
答:
- 连接点:程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring仅支持方法的连接点。
- 切点:如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。
- 增强:增强是织入到目标类连接点上的一段程序代码。Spring提供的增强接口都是带方位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。很多资料上将增强译为“通知”,这明显是个词不达意的翻译,让很多程序员困惑了许久。
- 引介:引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过引介功能,可以动态的未该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
- 织入:织入是将增强添加到目标类具体连接点上的过程,AOP有三种织入方式:
- 编译期织入:需要特殊的Java编译期(例如AspectJ的ajc)
- 类装载期织入:要求使用特殊的类加载器
- 动态代理织入:在运行时为目标类生成代理实现增强
Spring采用了动态代理织入,而AspectJ采用了编译期织入和类装载期织入的方式。
- 切面:切面是由切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义。
【补充】代理模式是GoF提出的23种设计模式中最为经典的模式之一,代理模式是对象的结构模式,它给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。简单的说,代理对象可以完成比原对象更多的职责,当需要为原对象添加横切关注功能时,就可以使用原对象的代理对象。我们在打开Office系列的Word文档时,如果文档中有插图,当文档刚加载时,文档中的插图都只是一个虚框占位符,等用户真正翻到某页要查看该图片时,才会真正加载这张图,这其实就是对代理模式的使用,代替真正图片的虚框就是一个虚拟代理;Hibernate的load方法也是返回一个虚拟代理对象,等用户真正需要访问对象的属性时,才向数据库发出SQL语句获得真实对象。代理模式的类图如下所示:
下面用一个找枪手代考的例子演示代理模式的使用:
- package com.lovo;
- /**
- * 参考人员接口
- * @author 骆昊
- *
- */
- public interface Candidate {
- /**
- * 答题
- */
- public void answerTheQuestions();
- }
- package com.lovo;
- /**
- * 懒学生
- * @author 骆昊
- *
- */
- public class LazyStudent implements Candidate {
- private String name; // 姓名
- public LazyStudent(String name) {
- this.name = name;
- }
- @Override
- public void answerTheQuestions() {
- // 懒学生只能写出自己的名字不会答题
- System.out.println("姓名: " + name);
- }
- }
- package com.lovo;
- /**
- * 枪手
- * @author 骆昊
- *
- */
- public class Gunman implements Candidate {
- private Candidate target; // 被代理对象
- public Gunman(Candidate target) {
- this.target = target;
- }
- @Override
- public void answerTheQuestions() {
- // 枪手要写上代考的学生的姓名
- target.answerTheQuestions();
- // 枪手要帮助懒学生答题并交卷
- System.out.println("奋笔疾书正确答案");
- System.out.println("交卷");
- }
- }
- package com.lovo;
- public class ProxyTest1 {
- public static void main(String[] args) {
- Candidate c = new Gunman(new LazyStudent("王小二"));
- c.answerTheQuestions();
- }
- }
从JDK 1.3开始,Java提供了动态代理技术,允许开发者在运行时创建接口的代理实例,主要包括Proxy类和InvocationHandler接口。下面的例子使用动态代理为ArrayList编写一个代理,在添加和删除元素时,在控制台打印添加或删除的元素以及ArrayList的大小:
- package com.lovo;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- import java.util.List;
- public class ListProxy<T> implements InvocationHandler {
- private List<T> target;
- public ListProxy(List<T> target) {
- this.target = target;
- }
- @Override
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- Object retVal = null;
- System.out.println("[" + method.getName() + ": " + args[0] + "]");
- retVal = method.invoke(target, args);
- System.out.println("[size=" + target.size() + "]");
- return retVal;
- }
- }
- package com.lovo;
- import java.lang.reflect.Proxy;
- import java.util.ArrayList;
- import java.util.List;
- public class ProxyTest2 {
- @SuppressWarnings("unchecked")
- public static void main(String[] args) {
- List<String> list = new ArrayList<String>();
- Class<?> clazz = list.getClass();
- ListProxy<String> myProxy = new ListProxy<String>(list);
- List<String> newList = (List<String>)
- Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), myProxy);
- newList.add("apple");
- newList.add("banana");
- newList.add("orange");
- newList.remove("banana");
- }
- }
使用Java的动态代理有一个局限性就是代理的类必须要实现接口,虽然面向接口编程是每个优秀的Java程序都知道的规则,但现实往往不尽如人意,对于没有实现接口的类如何为其生成代理呢?继承!继承是最经典的扩展已有代码能力的手段,虽然继承常常被初学者滥用,但继承也常常被进阶的程序员忽视。CGLib采用非常底层的字节码生成技术,通过为一个类创建子类来生成代理,它弥补了Java动态代理的不足,因此Spring中动态代理和CGLib都是创建代理的重要手段,对于实现了接口的类就用动态代理为其生成代理类,而没有实现接口的类就用CGLib通过继承的方式为其创建代理。
154. Spring中自动装配的方式有哪些?
答:
- no:不进行自动装配,手动设置Bean的依赖关系
- byName:根据Bean的名字进行自动装配
- byType:根据Bean的类型进行自动装配
- constructor:类似于byType,不过是应用于构造器的参数,如果正好有一个Bean与构造器的参数类型相同则可以自动装配,否则会导致错误
- autodetect:如果有默认的构造器,则通过constructor的方式进行自动装配,否则使用byType的方式进行自动装配
155. Spring中如何使用注解来配置Bean?有哪些相关的注解?
答:首先需要在Spring配置文件中增加如下配置:
- <context:component-scan base-package="org.example"/>
156. Spring支持的事务管理类型有哪些?你在项目中使用哪种方式?
答:Spring支持编程式事务管理和声明式事务管理。许多Spring框架的用户选择声明式事务管理,因为这种方式和应用程序的关联较少,因此更加符合轻量级容器的概念。声明式事务管理要优于编程式事务管理,尽管在灵活性方面它弱于编程式事务管理(编程式事务允许你通过代码控制业务)。
157. 如何在Web项目中配置Spring的IoC容器?
答:如果需要在Web项目中使用Spring的IoC容器,可以在Web项目配置文件web.xml中做出如下配置:
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
- </context-param>
- <listener>
- <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
- </listener>
158. 如何在Web项目中配置Spring MVC?
答:要使用Spring MVC需要在Web项目配置文件中配置其前端控制器DispatcherServlet,如下所示:
- <web-app>
- <servlet>
- <servlet-name>example</servlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <servlet-mapping>
- <servlet-name>example</servlet-name>
- <url-pattern>*.html</url-pattern>
- </servlet-mapping>
- </web-app>
【注意】上面的配置中使用了*.html的后缀映射,这样做一方面不能够通过URL推断采用了何种服务器端的技术,另一方面可以欺骗搜索引擎,因为搜索引擎不会搜索动态页面,这种做法可以称为伪静态化。
159. Spring MVC的工作原理是怎样的?
答:Spring MVC的工作原理如下图所示:
- 客户端的所有请求都交给前端控制器DispatcherServlet来处理,它会负责调用系统的其他模块来真正处理用户的请求。
- DispatcherServlet收到请求后,将根据请求的信息(包括URL、HTTP协议方法、请求头、请求参数、Cookie等)以及HandlerMapping的配置找到处理该请求的Handler(任何一个对象都可以作为请求的Handler)。当然,在这个地方Spring会通过HandlerAdapter对该处理器进行封装,HandlerAdapter是一个适配器,它用统一的接口对各种Handler中的方法进行调用。
- Handler完成对用户请求的处理后,会返回一个ModelAndView对象给DispatcherServlet,ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。当然,这里的视图是逻辑视图,DispatcherServlet还要借助ViewResolver完成从逻辑视图到真实视图对象的解析工作。
- 当得到真正的视图对象后,Dispatcher会利用视图对象对模型数据进行渲染。
- 客户端得到响应,可能是一个普通的HTML页面,也可以是XML或JSON字符串,还可以是一张图片或者一个PDF文件。
160. 如何在Spring IoC容器中配置数据源?
答:
- DBCP配置:
- <bean id="dataSource"
- class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="driverClassName" value="${jdbc.driverClassName}"/>
- <property name="url" value="${jdbc.url}"/>
- <property name="username" value="${jdbc.username}"/>
- <property name="password" value="${jdbc.password}"/>
- </bean>
- <context:property-placeholder location="jdbc.properties"/>
- C3P0配置:
- <bean id="dataSource"
- class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
- <property name="driverClass" value="${jdbc.driverClassName}"/>
- <property name="jdbcUrl" value="${jdbc.url}"/>
- <property name="user" value="${jdbc.username}"/>
- <property name="password" value="${jdbc.password}"/>
- </bean>
- <context:property-placeholder location="jdbc.properties"/>
161. 如何配置配置事务增强?
答:
- <?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:aop="http://www.springframework.org/schema/aop"
- xmlns:tx="http://www.springframework.org/schema/tx"
- xsi:schemaLocation="
- http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans.xsd
- http://www.springframework.org/schema/tx
- http://www.springframework.org/schema/tx/spring-tx.xsd
- http://www.springframework.org/schema/aop
- http://www.springframework.org/schema/aop/spring-aop.xsd">
- <!-- this is the service object that we want to make transactional -->
- <bean id="fooService" class="x.y.service.DefaultFooService"/>
- <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
- <tx:advice id="txAdvice" transaction-manager="txManager">
- <!-- the transactional semantics... -->
- <tx:attributes>
- <!-- all methods starting with 'get' are read-only -->
- <tx:method name="get*" read-only="true"/>
- <!-- other methods use the default transaction settings (see below) -->
- <tx:method name="*"/>
- </tx:attributes>
- </tx:advice>
- <!-- ensure that the above transactional advice runs for any execution
- of an operation defined by the FooService interface -->
- <aop:config>
- <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
- <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
- </aop:config>
- <!-- don't forget the DataSource -->
- <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
- <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
- <property name="username" value="scott"/>
- <property name="password" value="tiger"/>
- </bean>
- <!-- similarly, don't forget the PlatformTransactionManager -->
- <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
- <property name="dataSource" ref="dataSource"/>
- </bean>
- <!-- other <bean/> definitions here -->
- </beans>
162. 选择使用Spring框架的原因(Spring框架为企业级开发带来的好处)?
答:可以从以下几个方面作答:
- 非侵入式:支持基于POJO的编程模式,不强制性的要求实现Spring框架中的接口或继承Spring框架中的类。
- IoC容器:IoC容器帮助应用程序管理对象以及对象之间的依赖关系,对象之间的依赖关系如果发生了改变只需要修改配置文件而不是修改代码,因为代码的修改可能意味着项目的重新构建和完整的回归测试。有了IoC容器,程序员再也不需要自己编写工厂、单例,这一点特别符合Spring的精神“不要重复的发明轮子”。
- AOP:面向切面编程,将所有的横切关注功能封装到切面(aspect)中,通过配置的方式将横切关注功能动态添加到目标代码上,进一步实现了业务逻辑和系统服务之间的分离。另一方面,有了AOP程序员可以省去很多自己写代理类的工作。
- MVC:Spring的MVC框架是非常优秀的,从各个方面都可以甩Struts 2几条街,为Web表示层提供了更好的解决方案。
- 事务管理:Spring以宽广的胸怀接纳多种持久层技术(我想这一点与Rod Johnson音乐学博士的身份不无关系,这一点特别值得Gavin King学习,上天不可能赋予一个人太多的优点以至于他没有表达谦虚的余地。关于这二位的轶事可以自己百度一下),并且为其提供了声明式的事务管理,在不需要任何一行代码的情况下就能够完成事务管理。
- 其他:选择Spring框架的原因还远不止于此,Spring为Java企业级开发提供了一站式选择,你可以在需要的时候使用它的部分和全部,更重要的是,你甚至可以在感觉不到Spring存在的情况下,在你的项目中使用Spring提供的各种优秀的功能。
163. 依赖注入的方式以及你在项目中的选择?
答:依赖注入可以通过setter方法注入(设值注入)、构造器注入和接口注入三种方式来实现,Spring支持setter注入和构造器注入,通常使用构造器注入来注入必须的依赖关系,对于可选的依赖关系,则setter注入是更好的选择,setter注入需要类提供无参构造器或者无参的静态工厂方法来创建对象。
164. 提供Spring IoC容器配置元数据的方式?
答:
- 基于XML文件进行配置。
- 基于注解进行配置。
- 基于Java程序进行配置(Spring 3+)
- package com.lovo.bean;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- @Component
- public class Person {
- private String name;
- private int age;
- @Autowired
- private Car car;
- public Person(String name, int age) {
- this.name = name;
- this.age = age;
- }
- public void setCar(Car car) {
- this.car = car;
- }
- @Override
- public String toString() {
- return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
- }
- }
- package com.lovo.bean;
- import org.springframework.stereotype.Component;
- @Component
- public class Car {
- private String brand;
- private int maxSpeed;
- public Car(String brand, int maxSpeed) {
- this.brand = brand;
- this.maxSpeed = maxSpeed;
- }
- @Override
- public String toString() {
- return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
- }
- }
- package com.lovo.config;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import com.lovo.bean.Car;
- import com.lovo.bean.Person;
- @Configuration
- public class AppConfig {
- @Bean
- public Car car() {
- return new Car("Benz", 320);
- }
- @Bean
- public Person person() {
- return new Person("骆昊", 34);
- }
- }
- package com.lovo.test;
- import org.springframework.context.ConfigurableApplicationContext;
- import org.springframework.context.annotation.AnnotationConfigApplicationContext;
- import com.lovo.bean.Person;
- import com.lovo.config.AppConfig;
- class Test {
- public static void main(String[] args) {
- // TWR (Java 7+)
- try(ConfigurableApplicationContext factory = new AnnotationConfigApplicationContext(AppConfig.class)) {
- Person person = factory.getBean(Person.class);
- System.out.println(person);
- }
- }
- }
165. Spring提供了哪些Bean的作用域?
答:
- singleton:Bean以单例的方式存在(IoC容器中仅存在该Bean的唯一实例)。
- prototype:每次从容器中获取Bean的实例时,都会返回一个新的实例(原型模式)。
- request:每次HTTP请求都会创建新的实例,该作用域仅适用于WebApplicationContext环境。
- session:同一个HTTP会话共享一个Bean的实例,不同的HTTP会话使用不同的Bean实例,该作用域仅适用于WebApplicationContext环境。
- globalSession:同一个全局会话共享一个Bean的实例,一般应用于Portlet应用中。
166. 阐述Spring框架中Bean的生命周期?
答:
- Spring IoC容器找到关于Bean的定义并实例化该Bean。
- Spring IoC容器对Bean进行依赖注入。
- 如果Bean实现了BeanNameAware接口,则将该Bean的id传给setBeanName方法。
- 如果Bean实现了BeanFactoryAware接口,则将BeanFactory对象传给setBeanFactory方法。
- 如果Bean实现了BeanPostProcessor接口,则调用其postProcessBeforeInitialization方法。
- 如果Bean实现了InitializingBean接口,则调用其afterPropertySet方法。
- 如果有和Bean关联的BeanPostProcessors对象,则这些对象的postProcessAfterInitialization方法被调用。
- 当销毁Bean实例时,如果Bean实现了DisposableBean接口,则调用其destroy方法。
167. 依赖注入时如何注入集合属性?
答:可以在定义Bean属性时,通过<list>/<set>/<map>/<props>分别为其注入列表、集合、映射和键值都是字符串的映射属性。
168. Spring中的自动装配有哪些限制?
答:
- 如果使用了构造器注入或者setter注入,那么将覆盖自动装配的依赖关系。
- 基本数据类型的值、字符串字面量、类字面量无法使用自动装配来注入。
- 有先考虑使用显式的装配来进行更精确的依赖注入而不是使用自动装配。
169. 和自动装配相关的注解有哪些?
答:
- @Required:该依赖关系必须装配(手动或自动装配),否则将抛出BeanInitializationException异常。
- @Autowired:自动装配,默认按类型进行自动装配。
- @Qualifier:如果按类型自动装配时有不止一个匹配的类型,那么可以使用该注解指定名字来消除歧义。
170. 如何使用HibernateDaoSupport整合Spring和Hibernate?
答:
- 在Spring中配置Hibernate的会话工厂(LocalSessionFactoryBean或AnnotationSessionFactoryBean)。
- 让DAO的实现类继承HibernateDaoSupport(继承getHibernateTemplate方法来调用模板方法)。
- 让Spring来管理Hibernate的事务(推荐使用声明式事务)。
171. 你是如何理解“横切关注”这个概念的?
答:“横切关注”是会影响到整个应用程序的关注功能,它跟正常的业务逻辑是正交的,没有必然的联系,但是几乎所有的业务逻辑都会涉及到这些关注功能。通常,事务、日志、安全性等关注就是应用中的横切关注功能。
172. 如何理解Spring AOP中Advice这个概念?
答:Advice在国内的很多书面资料中都被翻译成“通知”,但是很显然这个翻译无法表达其本质,有少量的读物上将这个词翻译为“增强”,这个翻译是对Advice较为准确的诠释,我们通过AOP将横切关注功能加到原有的业务逻辑上,这就是对原有业务逻辑的一种增强,这种增强可以是前置增强、后置增强、返回后增强、抛异常时增强和包围型增强。
173. 在Web项目中如何获得Spring的IoC容器?
答:
- WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
174. Spring MVC如何对RESTful风格提供支持?
答:如果不了解RESTful可以看看百度百科的讲解,关于这个问题,可以看看blogjava上的另一个帖子。
175. 大型网站在架构上应当考虑哪些问题?
答:
- 分层:分层是处理任何复杂系统最常见的手段之一,将系统横向切分成若干个层面,每个层面只承担单一的职责,然后通过下层为上层提供的基础设施和服务以及上层对下层的调用来形成一个完整的复杂的系统。计算机网络的开放系统互联参考模型(OSI/RM)和Internet的TCP/IP模型都是分层结构,大型网站的软件系统也可以使用分层的理念将其分为持久层(提供数据存储和访问服务)、业务层(处理业务逻辑,系统中最核心的部分)和表示层(系统交互、视图展示)。需要指出的是:(1)分层是逻辑上的划分,在物理上可以位于同一设备上也可以在不同的设备上部署不同的功能模块,这样可以使用更多的计算资源来应对用户的并发访问;(2)层与层之间应当有清晰的边界,这样分层才有意义,才更利于软件的开发和维护。
- 分割:分割是对软件的纵向切分。我们可以将大型网站的不同功能和服务分割开,形成高内聚低耦合的功能模块(单元)。在设计初期可以做一个粗粒度的分割,将网站分割为若干个功能模块,后期还可以进一步对每个模块进行细粒度的分割,这样一方面有助于软件的开发和维护,另一方面有助于分布式的部署,提供网站的并发处理能力和功能的扩展。
- 分布式:除了上面提到的内容,网站的静态资源(JavaScript、CSS、图片等)也可以采用独立分布式部署并采用独立的域名,这样可以减轻应用服务器的负载压力,也使得浏览器对资源的加载更快。数据的存取也应该是分布式的,传统的商业级关系型数据库产品基本上都支持分布式部署,而新生的NoSQL产品几乎都是分布式的。当然,网站后台的业务处理也要使用分布式技术,例如查询索引的构建、数据分析等,这些业务计算规模庞大,可以使用Hadoop以及MapReduce分布式计算框架来处理。
- 集群:集群使得有更多的服务器提供相同的服务,可以更好的提供对并发的支持。
- 缓存:所谓缓存就是用空间换取时间的技术,将数据尽可能放在距离计算最近的位置。使用缓存是网站优化的第一定律。我们通常说的CDN、反向代理、热点数据都是对缓存技术的使用。
- 异步:异步是实现软件实体之间解耦合的又一重要手段。异步架构是典型的生产者消费者模式,二者之间没有直接的调用关系,只要保持数据结构不变,彼此功能实现可以随意变化而不互相影响,这对网站的扩展非常有利。使用异步处理还可以提高系统可用性,加快网站的响应速度(用Ajax加载数据就是一种异步技术),同时还可以起到削峰作用(应对瞬时高并发)。“能推迟处理的都要推迟处理”是网站优化的第二定律,而异步是践行网站优化第二定律的重要手段。
- 冗余:各种服务器都要提供相应的冗余服务器以便在某台或某些服务器宕机时还能保证网站可以正常工作,同时也提供了灾难恢复的可能性。冗余是网站高可用性的重要保证。
176. 你用过的网站前端优化的技术有哪些?
答:
- 浏览器访问优化
- 减少HTTP请求数量:合并CSS、合并JavaScript、合并图片(CSS Sprite)
- 使用浏览器缓存:通过设置HTTP响应头中的Cache-Control和Expires属性,将CSS、JavaScript、图片等在浏览器中缓存,当这些静态资源需要更新时,可以更新HTML文件中的引用来让浏览器重新请求新的资源
- 启用压缩
- CSS前置,JavaScript后置
- 减少Cookie传输
- CDN加速:CDN(Content Distribute Network)的本质仍然是缓存,将数据缓存在离用户最近的地方,CDN通常部署在网络运营商的机房,不仅可以提升响应速度,还可以减少应用服务器的压力。当然,CDN缓存的通常都是静态资源。
- 反向代理:反向代理相当于应用服务器的一个门面,可以保护网站的安全性,也可以实现负载均衡的功能,当然最重要的是它缓存了用户访问的热点资源,可以直接从反向代理将某些内容返回给用户浏览器。
177. 你使用过的应用服务器优化技术有哪些?
答:
- 分布式缓存:缓存的本质就是内存中的哈希表,如果设计一个优质的哈希函数,那么理论上哈希表读写的渐近时间复杂度为O(1)。缓存主要用来存放那些读写比很高、变化很少的数据,这样应用程序读取数据时先到缓存中读取,如果没有或者数据已经失效再去访问数据库或文件系统,并根据拟定的规则将数据写入缓存。对网站数据的访问也符合二八定律(Pareto分布,幂律分布),即80%的访问都集中在20%的数据上,如果能够将这20%的数据缓存起来,那么系统的性能将得到显著的改善。当然,使用缓存需要解决以下几个问题:(1)频繁修改的数据;(2)数据不一致与脏读;(3)缓存雪崩(可以采用分布式缓存服务器集群加以解决,Memcached是广泛采用的解决方案,它是一种互不通信的集中式管理的分布式缓存方案,可以从http://memcached.org/了解到关于Memcached的相关信息);(4)缓存预热;(5)缓存穿透(恶意持续请求不存在的数据)。
- 异步操作:可以使用消息队列将调用异步化,通过异步处理将短时间高并发产生的事件消息存储在消息队列中,从而起到削峰作用。电商网站在进行促销活动时,可以将用户的订单请求存入消息队列,这样可以抵御大量的并发订单请求对系统和数据库的冲击。目前,绝大多数的电商网站即便不进行促销活动,订单系统都采用了消息队列来处理。
- 使用集群
- 代码优化
- 多线程:基于Java的Web开发基本上都通过多线程的方式响应用户的并发请求,使用多线程技术在编程上要解决线程安全问题,主要可以考虑以下几个方面:
- 将对象设计为无状态对象(这和面向对象的编程观点是矛盾的,在面向对象的世界中被视为不良设计),这样就不会存在并发访问时对象状态不一致的问题。
- 在方法内部创建对象,这样对象由进入方法的线程创建,不会出现多个线程访问同一对象的问题。使用ThreadLocal将对象与线程绑定也是很好的做法,这一点在前面已经探讨过了。
- 对资源进行并发访问时应当使用合理的锁机制。
- 非阻塞I/O: 使用单线程和非阻塞I/O是目前公认的比多线程的方式更能充分发挥服务器性能的应用模式,基于Node.js构建的服务器就采用了这样的方式。Java在JDK 1.4中就引入了NIO(Non-blocking I/O),在Servlet 3规范中又引入了异步Servlet的概念,这些都为在服务器端采用非阻塞I/O提供了必要的基础。
- 资源复用:资源复用主要有两种方式,一是单例,而是对象池,我们使用的数据库连接池、线程池都是对象池化技术,这是典型的用空间换取时间的策略,另一方面也实现对资源的复用,从而避免了不必要的创建和释放资源所带来的开销。
- 多线程:基于Java的Web开发基本上都通过多线程的方式响应用户的并发请求,使用多线程技术在编程上要解决线程安全问题,主要可以考虑以下几个方面:
178. 什么是XSS攻击?什么是SQL注入攻击?什么是CSRF攻击?
答:
- XSS(Cross Site Script,跨站脚本攻击)是向网页中注入恶意脚本在用户浏览网页时在用户浏览器中执行恶意脚本的攻击方式。跨站脚本攻击分有两种形式:反射型攻击(诱使用户点击一个嵌入恶意脚本的链接以达到攻击的目标,目前有很多攻击者利用论坛、微博发布含有恶意脚本的URL就属于这种方式)和持久型攻击(将恶意脚本提交到被攻击网站的数据库中,用户浏览网页时,恶意脚本从数据库中被加载到页面执行,QQ邮箱的早期版本就曾经被利用作为持久型跨站脚本攻击的平台)。XSS虽然不是什么新鲜玩意,但是攻击的手法却不断翻新,防范XSS主要有两方面:消毒(对危险字符进行转义)和HttpOnly(防范XSS攻击者窃取Cookie数据)。
- SQL注入攻击是注入攻击最常见的形式(此外还有OS注入攻击(Struts 2的高危漏洞就是通过OGNL实施OS注入攻击导致的)),当服务器使用请求参数构造SQL语句时,恶意的SQL被嵌入到SQL中交给数据库执行。SQL注入攻击需要攻击者对数据库结构有所了解才能进行,攻击者想要获得表结构有多种方式:(1)如果使用开源系统搭建网站,数据库结构也是公开的(目前有很多现成的系统可以直接搭建论坛,电商网站,虽然方便快捷但是风险是必须要认真评估的);(2)错误回显(如果将服务器的错误信息直接显示在页面上,攻击者可以通过非法参数引发页面错误从而通过错误信息了解数据库结构,Web应用应当设置友好的错误页,一方面符合最小惊讶原则,一方面屏蔽掉可能给系统带来危险的错误回显信息);(3)盲注。防范SQL注入攻击也可以采用消毒的方式,通过正则表达式对请求参数进行验证,此外,参数绑定也是很好的手段,这样恶意的SQL会被当做SQL的参数而不是命令被执行,JDBC中的PreparedStatement就是支持参数绑定的语句对象,从性能和安全性上都明显优于Statement。
- CSRF攻击(Cross Site Request Forgery,跨站请求伪造)是攻击者通过跨站请求,以合法的用户身份进行非法操作(如转账或发帖等)。CSRF的原理是利用浏览器的Cookie或服务器的Session,盗取用户身份,其原理如下图所示。防范CSRF的主要手段是识别请求者的身份,主要有以下几种方式:(1)在表单中添加令牌(token);(2)验证码;(3)检查请求头中的Referer(前面提到防图片盗链接也是用的这种方式)。令牌和验证都具有一次消费性的特征,因此在原理上一致的,但是验证码是一种糟糕的用户体验,不是必要的情况下不要轻易使用验证码,目前很多网站的做法是如果在短时间内多次提交一个表单未获得成功后才要求提供验证码,这样会获得较好的用户体验。
和充血模型(rich domain model)有什么区别?
- 它是一个大多数开发者都能够理解的简单过程模型(适合国内的绝大多数开发者)。
- 它能够与一个使用行数据入口或表数据入口的简单数据访问层很好的协作。
- 事务边界的显而易见,一个事务开始于脚本的开始,终止于脚本的结束,很容易通过代理(或切面)实现声明式事务。
- 更清晰的代码 --- 只写需要的代码
- 更好的设计
- 更出色的灵活性 --- 鼓励程序员面向接口编程
- 更快速的反馈 --- 不会到系统上线时才知道bug的存在
TDD可以在多个层级上应用,包括单元测试(测试一个类中的代码)、集成测试(测试类之间的交互)、系统测试(测试运行的系统)和系统集成测试(测试运行的系统包括使用的第三方组件)。TDD的实施步骤是:红(失败测试) --- 绿(通过测试) --- 重构。关于实施TDD的详细步骤请参考另一篇文章《测试驱动开发之初窥门径》。
在使用TDD开发时,经常会遇到需要被测对象需要依赖其他子系统的情况,但是你希望将测试代码跟依赖项隔离,以保证测试代码仅仅针对当前被测对象或方法展开,这时候你需要的是测试替身。测试替身可以分为四类:
- 虚设替身:只传递但是不会使用到的对象,一般用于填充方法的参数列表
- 存根替身:总是返回相同的预设响应,其中可能包括一些虚设状态
- 伪装替身:可以取代真实版本的可用版本(比真实版本还是会差很多)
- 模拟替身:可以表示一系列期望值的对象,并且可以提供预设响应