基本功

1. 面向对象的特征

1.1 三大特征
1.1.1 封装
  • 定义

    封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。

    要访问该类的代码和数据,必须通过严格的接口控制。

    封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。

    适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。

  • 优点

    • 良好的封装能够减少耦合。
    • 类内部的结构可以自由修改。
    • 可以对成员变量进行更精确的控制。
    • 隐藏信息,实现细节。
1.1.2 继承
  • 定义

    继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为

  • 特性

    • 子类拥有父类非 private 的属性、方法。
    • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
    • 子类可以用自己的方式实现父类的方法。
    • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 A 类继承 B 类,B 类继承 C 类,所以按照关系就是 C 类是 B 类的父类,B 类是 A 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
    • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
  • 关键字

    • extends:类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类
    • implements:可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)
1.1.3 多态性
  • 定义

    多态是同一个行为具有多个不同表现形式或形态的能力。

    多态就是同一个接口,使用不同的实例而执行不同操作。

  • 优点

    • 消除类型之间的耦合关系
    • 可替换性
    • 可扩充性
    • 接口性
    • 灵活性
    • 简化性
  • 多态存在的三个必要条件

    • 继承
    • 重写
    • 父类引用指向子类对象
  • 多态的实现方式

    • 重写
    • 接口
    • 抽象类和抽象方法
1.2 五大原则
  • 单一职责原则(Single-Resposibility Principle) 一个类应该仅有一个引起它变化的原因
    • 类的复杂性降低,实现什么职责都有清晰明确的定义
    • 可读性提高,因为复杂性减低了
    • 可维护性提高,因为可读性提高了
    • 变更引起的风险降低,变更是必不可少的,如果接口单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的可拓展性,维护性都有非常大的帮助
  • 开放封闭原则(Open-Closed principle) 对扩展是开放的,对更改是封闭的
    -
  • 里氏替换原则(Liskov-Substituion Principle) 子类可以替换父类并且出现在父类能够出现的任何地方,贯彻GOF倡导的面向接口编程
  • 依赖倒置原则(Dependecy-Inversion Principle) 高层模块不依赖低层模块
  • 接口隔离原则(Interface-Segregation Principle) 使用多个专门的接口比使用单个接口要好的多

2. final,fianlly,finalize的区别

2.1 简单区别
  • final用于声明属性,方法和类,分别表示属性不可交变,方法不可覆盖,类不可继承。

  • finally是异常处理语句结构的一部分,表示总是执行。

  • finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。

2.2 中等区别
  • final:java中的关键字,修饰符。

      1. 如果一个类被声明为final,就意味着它不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为absrtact抽象类的和final的类。
      1. 如果将变量或者方法声明为final,可以保证它们在使用中不被改变。

        2.1 被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。

        2.2 被声明final的方法只能使用,不能重载。

  • finally:java的一种异常处理机制。

    finally是对Java 异常处理模型的最佳补充。finally 结构使代码总会执行,而不管有无异常发生。使用 finally 可以维护对象的内部状态,并可以清理非内存资源。特别是在关闭数据库连接这方面,如果程序员把数据库连接的close()方法放到finally中,就会大大降低程序出错的几率。

  • finalize:java中的一个方法名。

    Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

2.3 详细区别
2.3.1.final

它可以用于以下四个地方:1.定义变量,包括静态的和非静态的。2.定义方法的参数。3.定义方法。4.定义类。

  1. 定义变量,包括静态的和非静态的 and 定义方法的参数

    如果final修饰的是一个基本类型,就表示这个变量被赋予的值是不可变的,即它是个常量;

    如果final修饰的是一个对象,就表示这个变量被赋予的引用是不可变的。这里需要提醒大家注意的是,不可改变的只是这个变量所保存的引用,并不是这个引用所指向的对象。

    实际上对于前两种情况,有一种更贴切的表述final的含义的描述,那就是,如果一个变量或方法参数被final修饰,就表示它只能被赋值一次,但是JAVA虚拟机为变量设定的默认值不记作一次赋值。

    • 被final修饰的变量必须被初始化。初始化的方式有以下几种:
      • 在定义的时候初始化。
      • final变量可以在初始化块中初始化,不可以在静态初始化块中初始化。
      • 静态final变量可以在静态初始化块中初始化,不可以在初始化块中初始化。
      • final变量还可以在类的构造器中初始化,但是静态final变量不可以。
  2. 定义方法

    它表示这个方法不可以被子类重写,但是它这不影响它被子类继承。具有private访问权限的方法也可以增加final修饰,但是由于子无法继承private方法,因此也无法重写它。编译器在处理private方法时,是按照final方来对待的,这样可以提高该方法被调用时的效率。不过子类仍然可以定义同父类中private方法具有同样结构的方法,但是这并不会产生重写的效果,而且它们之间也不存在必然联系。

  3. 定义类

    String类就是final的。由于final类不允许被继承,编译器在处理时把它的所有方法都当作final的,因此final类比普通类拥有更高的效率。而由关键字abstract定义的抽象类含有必须由继承自它的子类重载实现的抽象方法,因此无法同时用final和abstract来修饰同一个类。同样的道理,final也不能用来修饰接口。 final的类的所有方法都不能被重写,但这并不表示final的类的属性(变量)值也是不可改变的,要想做到final类的属性值不可改变,必须给它增加final修饰

2.3.2 fianlly

return、continue和break都不能阻止finally语句块的执行。

return语句被执行后就已经退出当前方法了,finally语句块又如何能被执行呢?

因此,正确的执行顺序应该是这样的:编译器在编译return new ReturnClass();时,将它分成了两个步骤,new ReturnClass()和return,前一个创建对象的语句是在finally语句块之前被执行的,而后一个return语句是在finally语句块之后执行的,也就是说finally语句块是在程序退出方法之前被执行的。

同样,finally语句块是在循环被跳过(continue)和中断(break)之前被执行的。

2.3.3 finalize

它是一个方法,属于java.lang.Object类,它的定义如下:Java代码protected void finalize() throws Throwable { }众所周知,finalize()方法是GC(garbage collector)运行机制的一部分在此我们只说说finalize()方法的作用是什么呢?finalize()方法是在GC清理它所从属的对象时被调用的,如果执行它的过程中抛出了无法捕获的异常(uncaught exception),GC将终止对改对象的清理,并且该异常会被忽略;直到下一次GC开始清理这个对象时,它的finalize()会被再次调用。

3. int和Integer的区别与比较

3.1 int和Integer的区别
  • Integer是int的包装类,int则是java的一种基本数据类型
  • Integer变量必须实例化后才能使用,而int变量不需要
  • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
  • Integer的默认值是null,int的默认值是0
3.2 Integer和int的比较

1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。

1
2
3
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false

2、Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)

1
2
3
Integer i = new Integer(100);
int j = 100
System.out.print(i == j); //true

3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)

1
2
3
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false

4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false

1
2
3
4
5
6
7
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true

Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false

对于第4条的原因:
java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:

1
2
3
4
5
6
7
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}

java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。

4. 重载(Overload)和重写(Override)的区别

4.1 重写(Override)

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。

4.1.1 重写规则
  • 参数列表必须完全与被重写方法的相同;
  • 返回类型必须完全与被重写方法的返回类型相同;
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为final的方法不能被重写。
  • 声明为static的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个方法,则不能重写这个方法。
4.2 重载(Overload)

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

最常用的地方就是构造器的重载。

4.2.1 重载规则
  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。
4.3 区别
区别点 重载方法 重写方法
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
异常 可以修改 可以减少或删除,一定不能抛出新的或者更广的异常
访问 可以修改 一定不能做更严格的限制(可以降低限制)
4.4 总结

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

5. 抽象类和接口有什么区别

5.1 抽象类

抽象类是用来捕捉子类的通用特性的 。它不能被实例化,只能被用作子类的超类。抽象类是被用来创建继承层级里子类的模板。

5.2 接口

接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的抽象方法。这就像契约模式,如果实现了这个接口,那么就必须确保使用这些方法。接口只是一种形式,接口自身不能做任何事情。

5.3 抽象类和接口的对比
参数 抽象类 接口
默认的方法实现 它可以有默认的方法实现 接口完全是抽象的。它根本不存在方法的实现
实现 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现
构造器 抽象类可以有构造器 接口不能有构造器
与正常Java类的区别 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 接口是完全不同的类型
访问修饰符 抽象方法可以有publicprotecteddefault这些修饰符 接口方法默认修饰符是public。你不可以使用其它修饰符。
main方法 抽象方法可以有main方法并且我们可以运行它 接口没有main方法,因此我们不能运行它。
多继承 抽象方法可以继承一个类和实现多个接口 接口只可以继承一个或多个其它接口
速度 它比接口速度要快 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
添加新方法 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 如果你往接口中添加方法,那么你必须改变实现该接口的类。
5.4 什么时候使用抽象类和接口
  • 如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧。
  • 如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
  • 如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。
5.5 Java8中的默认方法和静态方法

Oracle已经开始尝试向接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。现在,我们可以为接口提供默认实现的方法了并且不用强制子类来实现它。这类内容我将在下篇博客进行阐述。

6. 说说反射的用途及实现

Java反射机制主要提供了以下功能:在运行时构造一个类的对象;判断一个类所具有的成员变量和方法;调用一个对象的方法;生成动态代理。反射最大的应用就是框架

反射的应用Spring 的 ioc/di,JavaBean和Jsp之间调用,Struts的 FormBean 和页面之间,JDBC 的 classForName(),Hibernate的 find(Class clazz) 。

6.1 Java的反射机制
  • 在运行时判断任意一个对象所属的类;

  • 在运行时构造任意一个类的对象;

  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);

  • 在运行时调用任意一个对象的方法

重点:是运行时而不是编译时

6.2 反射的一些注意事项
  • 由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。

  • 另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

6.3 反射的实现

获取一个对象对应的反射类,在 Java 中有下列方法可以获取一个对象的反射类

  1. 通过 getClass() 方法
  2. 通过 Class.forName() 方法
  3. 使用 类.class
  4. 通过类加载器实现,getClassLoader()
1
2
3
4
5
6
7
8
9
10
//通过getClass方法
String s = "text";
Class<?> c = s.getClass();

//通过forName方法
Class<?> c1 = Class.forName("java.lang.String");

//使用.class
Class<String> c2 = String.class;
Class<Integer> type = Integer.TYPE;
6.4 反射的用途

Java 反射机制是一个非常强大的功能,在很多的项目比如 Spring,MyBatis 都都可以看到反射的身影。通过反射机制,我们可以在运行期间获取对象的类型信息。利用这一点我们可以实现工厂模式和代理模式等设计模式,同时也可以解决 Java 泛型擦除等令人苦恼的问题。

6.4.1 判断是否为某个类的实例
1
2
List<String> testList = new ArrayList<>();
System.out.println(ArrayList.class.isInstance(testList));
6.4.2 创建实例
1
2
3
4
5
6
String str = c2.newInstance();
System.out.println("newInstance 创建实例 : " + str);
//------
Constructor<String> constructor = c2.getConstructor(String.class);
String test = constructor.newInstance("test");
System.out.println("constructor 创建实例 : " + test);
6.4.3 获取方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Class<MethodClass> clazz = MethodClass.class;
MethodClass methodClass = clazz.newInstance();
Method[] methods = clazz.getMethods();
Method[] declaredMethods = clazz.getDeclaredMethods();
//获取methodClass类的add方法
Method method = clazz.getMethod("add", int.class, int.class);
System.out.println(method.invoke(methodClass, 1, 2));
//getMethods()方法获取的所有方法
System.out.println("getMethods获取的方法:");
for (Method m : methods) {
System.out.println(m);
}
//getDeclaredMethods()方法获取的所有方法
System.out.println("getDeclaredMethods获取的方法:");
for (Method m : declaredMethods) {
System.out.println(m);
}
6.4.4 利用反射创建数组
1
2
3
4
5
6
7
8
9
10
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
//往数组里添加内容
Array.set(array,0,"hello");
Array.set(array,1,"Java");
Array.set(array,2,"fuck");
Array.set(array,3,"Scala");
Array.set(array,4,"Clojure");
//获取某一项的内容
System.out.println(Array.get(array,3));

7. 说说自定义注解的场景及实现

登陆、权限拦截、日志处理,以及各种Java框架,如Spring,Hibernate,JUnit 提到注解就不能不说反射,Java自定义注解是通过运行时靠反射获取注解。实际开发中,例如我们要获取某个方法的调用日志,可以通过AOP(动态代理机制)给方法添加切面,通过反射来获取方法包含的注解,如果包含日志注解,就进行日志记录。

7.1 元注解
  • @Target: 作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
1
2
3
4
5
6
7
8
取值(ElementType)有:
    1.CONSTRUCTOR:用于描述构造器
    2.FIELD:用于描述域
    3.LOCAL_VARIABLE:用于描述局部变量
    4.METHOD:用于描述方法
    5.PACKAGE:用于描述包
    6.PARAMETER:用于描述参数
    7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
  • @Retention: 作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
1
2
3
4
取值(RetentionPoicy)有:
    1.SOURCE:在源文件中有效(即源文件保留)
    2.CLASS:在class文件中有效(即class保留)
    3.RUNTIME:在运行时有效(即运行时保留)
  • @Documented: 用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。

  • @Inherited: 如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

7.2 代码实践
7.2.1 自定义一个注解
1
2
3
4
5
6
7
// 定义@NeedTest注解的保留期限,该注解会保存到目标类的字节码中,并且会被类加载器加载到JVM中
@Retention(RetentionPolicy.RUNTIME)
// 定义@NeedTest注解的应用目标,这是一个方法级别的注解
@Target(ElementType.METHOD)
public @interface NeedTest {
boolean value() default true; // 单个成员,成员名必须是value(), 默认值是true
}
7.2.2 创建一个类来使用该注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyService {
public void saySomething() {
System.out.println("say something");
}

@NeedTest(true) // 成员名value()设置为true
public void sayHello(String name) {
System.out.println("hello " + name);
}

@NeedTest(false) // 成员名value()设置为false
public void sayHi(String name) {
System.out.println("hi " + name);
}
}
7.2.3 创建测试类,获取注解的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
public static void main(String[] args) {
Class clazz = MyService.class;
Method[] methods = clazz.getDeclaredMethods();
if (methods.length == 0) {
System.out.println("method " + clazz.getName() + " has no declared method");
} else {
for (Method method : methods) {
// 所有自定义的注解都隐式继承自java.lang.annotation.Annotation接口,但是不允许显示继承其他接口
NeedTest annotation = method.getAnnotation(NeedTest.class);
if (annotation == null) {
System.out.println("method" + method.getName() + " has not annotated @NeedTest");
} else {
boolean value = annotation.value();
System.out.println(method.getName() + " has annotated @NeedTest and value = " + value);
}
}
}
}
}

8. HTTP 请求的 GET 与 POST 方式的区别

8.1 get

GET方法会把名值对追加在请求的URL后面。因为URL对字符数目有限制,进而限制了用在客户端请求的参数值的数目。并且请求中的参数值是可见的,因此,敏感信息不能用这种方式传递。

8.2 post

POST方法通过把请求参数值放在请求体中来克服GET方法的限制,因此,可以发送的参数的数目是没有限制的。最后,通过POST请求传递的敏感信息对外部客户端是不可见的。

8.3 区别

  • 最直观的就是语义上的区别,get用于获取数据,post用于提交数据。

  • get参数有长度限制(受限于url长度,具体的数值取决于浏览器和服务器的限制),而post无限制

cookie 是 Web 服务器发送给浏览器的一块信息。浏览器会在本地文件中给每一个 Web 服务器存储cookie。以后浏览器在给特定的 Web 服务器发请求的时候,同时会发送所有为该服务器存储的 cookie。

下面列出了 session 和 cookie 的区别:
无论客户端浏览器做怎么样的设置,session都应该能正常工作。客户端可以选择禁用 cookie,但是, session 仍然是能够工作的,因为客户端无法禁用服务端的 session。

  1. cookie数据存放在客户的浏览器上,session数据放在服务器上。
  2. cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,如果主要考虑到安全应当使用session。
  3. session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用cookie。
  4. 单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的cookie不能超过3K。
  5. 一般将登陆信息等重要信息存放为session;其他信息如果需要保留,可以放在cookie中

10. session 分布式处理

配置参考资料:http://xstarcd.github.io/wiki/Java/tomcat_cluster.html

10.1 分布式架构下,session会有什么问题

搭建完集群环境后,不得不考虑的一个问题就是用户访问产生的session如何处理。如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在A、B两台服务器,用户在第一次访问网站时,Nginx通过其负载均衡机制将用户请求转发到A服务器,这时A服务器就会给用户创建一个Session。当用户第二次发送请求时,Nginx将其负载均衡到B服务器,而这时候B服务器并不存在Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。

我们应当对产生的Session进行处理,通过粘性Session,Session复制或Session共享等方式保证用户的体验度。

10.2 粘性session
  • 原理:粘性Session是指将用户锁定到某一个服务器上,比如上面说的例子,用户第一次请求时,负载均衡器将用户的请求转发到了A服务器上,如果负载均衡器设置了粘性Session的话,那么用户以后的每次请求都会转发到A服务器上,相当于把用户和A服务器粘到了一块,这就是粘性Session机制。

  • 优点:简单,不需要对session做任何处理。

  • 缺点:缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的session信息都将失效。

  • 适用场景:发生故障对客户产生的影响较小;服务器发生故障是低概率事件。

  • 实现方式:以Nginx为例,在upstream模块配置ip_hash属性即可实现粘性Session。

1
2
3
4
5
6
upstream mycluster{
#这里添加的是上面启动好的两台Tomcat服务器
ip_hash;#粘性Session
server 192.168.22.229:8080 weight=1;
server 192.168.22.230:8080 weight=1;
}
10.3 服务器session复制
  • 原理:任何一个服务器上的session发生改变(增删改),该节点会把这个 session的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要session,以此来保证Session同步。

  • 优点:可容错,各个服务器间session能够实时响应。

  • 缺点:会对网络负荷造成一定压力,如果session量大的话可能会造成网络堵塞,拖慢服务器性能。

  • 实现方式

    ① 设置tomcat,server.xml 开启tomcat集群功能,address:填写本机ip即可,设置端口号,预防端口冲突。

    ② 在应用里增加信息:通知应用当前处于集群环境中,支持分布式 在web.xml中添加选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!--server.xml -->
<!--下面的代码是实现session复制功能-->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="6">
<Manager className="org.apache.catalina.ha.session.BackupManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"
mapSendOptions="6"/>
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4"
port="45564"
frequency="500"
dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="192.168.100.63" <!—这里填写本机IP地址-->
port="5000"
selectorTimeout="100" />
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/>
</Channel>

<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter=".*\.gif;.*\.js;.*\.jpg;.*\.png;.*\.htm;.*\.html;.*\.css;.*\.txt;"/>
<Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/"
watchDir="/tmp/war-listen/"
watchEnabled="false"/>
<ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>
1
2
3
4
5
<!--web.xml -->
<web-app>
<!-- -->
<distributable /> <!--增加这个代码才能实现session同步复制功能-->
</web-app>
10.4 session共享机制

使用分布式缓存方案比如memcached、Redis,但是要求Memcached或Redis必须是集群。 使用Session共享也分两种机制,两种情况如下:

10.4.1 粘性session处理方式
  • 原理:不同的 tomcat指定访问不同的主memcached。多个Memcached之间信息是同步的,能主从备份和高可用。用户访问时首先在tomcat中创建session,然后将session复制一份放到它对应的memcahed上。memcache只起备份作用,读写都在tomcat上。当某一个tomcat挂掉后,集群将用户的访问定位到备tomcat上,然后根据cookie中存储的sessionId找session,找不到时,再去相应的memcached上去session,找到之后将其复制到备tomcat上。

  • 配置

    1
    2
    3
    4
    5
    6
    7
    <Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
    memcachedNodes="n1:192.168.100.208:11211 n2:192.168.100.208:11311"
    failoverNodes="n1"
    requestUriIgnorePattern=".*\.(png|gif|jpg|css|js)$"
    memcachedProtocol="binary"
    transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
    />

粘性session处理方式

10.4.2 非粘性session处理方式
  • 原理:memcached做主从复制,写入session都往从memcached服务上写,读取都从主memcached读取,tomcat本身不存储session

  • 优点:可容错,session实时响应。

  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    <Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
    memcachedNodes="n1:192.168.100.208:11211 n2:192.168.100.208:11311"
    lockingMode="auto"
    sticky="false"
    requestUriIgnorePattern= ".*\.(png|gif|jpg|css|js)$"
    sessionBackupAsync= "false"
    memcachedProtocol= "binary" transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
    />

非粘性session处理方式

实现方式:用开源的msm插件解决tomcat之间的session共享:

1
2
3
4
5
6
7
8
9
10
11
Memcached_Session_Manager(MSM)
a. 复制相关jar包到tomcat/lib 目录下
JAVA memcached客户端:spymemcached.jar

msm项目相关的jar包:

1. 核心包,memcached-session-manager-{version}.jar
2. Tomcat版本对应的jar包:memcached-session-manager-tc{tomcat-version}-{version}.jar

序列化工具包:可选kryo,javolution,xstream等,不设置时使用jdk默认序列化。
b. 配置Context.xml ,加入处理Session的Manager
10.5 session持久化到数据库

原理:就不用多说了吧,拿出一个数据库,专门用来存储session信息。保证session的持久化。 优点:服务器出现问题,session不会丢失 缺点:如果网站的访问量很大,把session存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库.

10.6 terracotta实现session复制
  • 原理:Terracotta的基本原理是对于集群间共享的数据,当在一个节点发生变化的时候,Terracotta只把变化的部分发送给Terracotta服务器,然后由服务器把它转发给真正需要这个数据的节点。可以看成是对第二种方案的优化。

  • 优点:这样对网络的压力就非常小,各个节点也不必浪费CPU时间和内存进行大量的序列化操作。把这种集群间数据共享的机制应用在session同步上,既避免了对数据库的依赖,又能达到负载均衡和灾难恢复的效果。

terracotta实现session复制
terracotta实现session复制
10.7 总结

session的5种处理策略。其中就应用广泛性而言,第三种方式,也就是基于第三方缓存框架共享session,应用的最为广泛,无论是效率还是扩展性都很好。而Terracotta作为一个JVM级的开源群集框架,不仅提供HTTP Session复制,它还能做分布式缓存,POJO群集,跨越群集的JVM来实现分布式应用程序协调等,也值得学习一下。

11. JDBC 流程

11.1 加载Driver类,注册数据库驱动;
1
2
3
4
5
6
7
try{
//加载MySql的驱动类
Class.forName("com.mysql.jdbc.Driver") ;
}catch(ClassNotFoundException e){
System.out.println("找不到驱动程序类 ,加载驱动失败!");
e.printStackTrace() ;
}
11.2 通过DriverManager,使用url,用户名和密码建立连接(Connection);
  • 连接URL定义了连接数据库时的协议、子协议、数据源标识。

  • 书写形式:协议:子协议:数据源标识

    • 协议:在JDBC中总是以jdbc开始
    • 子协议:是桥连接的驱动程序或是数据库管理系统名称。
    • 数据源标识:标记找到数据库来源的地址与连接端口。

例如:

jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=gbk;useUnicode=true;(MySql的连接URL)

useUnicode表示使用Unicode字符集,如果字符编码方式characterEncoding设置为 gb2312或GBK,本参数必须设置为true 。

要连接数据库,需要向java.sql.DriverManager请求并获得Connection对象, 该对象就代表一个数据库的连接。

1
2
3
4
5
6
7
8
9
String url = "jdbc:mysql://localhost:3306/test" ;
String username = "root" ;
String password = "root" ;
try{
Connection con = DriverManager.getConnection(url , username , password ) ;
}catch(SQLException se){
System.out.println("数据库连接失败!");
se.printStackTrace() ;
}
11.3 通过Connection,使用sql语句打开Statement对象;

要执行SQL语句,必须获得java.sql.Statement实例,Statement实例分为以下3 种类型:

1、执行静态SQL语句。通常通过Statement实例实现。

1
Statement stmt = con.createStatement() ;

2、执行动态SQL语句。通常通过PreparedStatement实例实现。

1
2
ps = connection.prepareStatement("select * from student where age =?");
ps.setInt(1, 10);

3、执行数据库存储过程。通常通过CallableStatement实例实现。

1
CallableStatement cstmt = con.prepareCall("{CALL demoSp(? , ?)}") ;
11.4 执行语句,将结果返回resultSet;
1
ResultSet resultSet = ps.executeQuery();
11.5 对结果resultSet进行处理;
1
2
3
4
5
while (resultSet.next()) {
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
System.out.println("name : " + name + " ,age : " + age);
}
11.6 倒序释放资源resultSet -> preparedStatement -> connection。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
try {}
catch(){}
finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

12. MVC 设计思想

MVC指的就是Model-View-Controller(模型-视图-控制器)

13. equals 与 == 的区别

值类型(int,char,long,boolean等)都是用 == 判断相等性。对象引用的话,== 判断引用所指的对象是否是同一个。equals是Object的成员函数,有些类会覆盖(override)这个方法,用于判断对象的等价性。例如String类,两个引用所指向的String都是”abc”,但可能出现他们实际对应的对象并不是同一个(和jvm实现方式有关),因此用==判断他们可能不相等,但用equals判断一定是相等的。

13.1 基本数据类型,也称原始数据类型。

byte,short,char,int,long,float,double,boolean
他们之间的比较,应用双等号==,比较的是他们的值。

13.2 复合数据类型(类)

当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。

JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。
对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。