@MRsunhuimin
2022-02-12T01:13:52.000000Z
字数 53525
阅读 1049
面试问题总结
包 com.公司名.项目名.包名
大骆驼命名法 类 UserService
小骆驼命名法 方法 shouName()
属性 userName user_name username $username
高内聚 低耦合
多聚合 少继承
高内聚:属于一个类的方法,尽量的写在这个类里
低耦合:模块与模块之间联系不要过高
多聚合:在一个类中把其他类当成成员变量来使用(其他类已定义的属性尽量不要再定义 )
1.集合和数组都是JAVA中的容器
2.数组特点:大小固定,并且同一个数组只能存放相同数据类型的数据(基本数据类型/引用数据类型)
3.集合特点:大小可动态扩展,可以存储各种类型的数据。
4.联系:使用相应的toArray()和Arrays.asList()方法可以互相转换。
迭代器模式:就是提供一种方法对一个容器对象中的各个元素进行访问,而又不暴露该对象容器的内部细节
迭代器与枚举有两点不同:
1. 迭代器在迭代期间可以从集合中移除元素。
2. 方法名得到了改进,Enumeration的方法名称都比较长。
1.在固定长度或者长度不需要计算的时候for循环效率高于foreach,在不确定长度或者计算长度有损性能的时候用foreach比较方便
1.在固定长度或者长度不需要计算的时候for循环效率高于foreach,
在不确定长度或者计算长度有损性能的时候用foreach比较方便
2.foreach适用于只是进行集合或数组遍历,
for则在较复杂的循环中效率更高。
3.如果对集合中的值进行修改,就要用for循环了。
其实foreach的内部原理其实也是Iterator,但它不能像Iterator一样可以人为的控制,而且也不能调用iterator.remove();更不能使用下标来访问每个元素,所以不能用于增加,删除等复杂的操作。
4.forEach相比普通的for循环的优势在于对稀疏数组的处理,会跳过数组中的空位。
4.1 区别体现
这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。
首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String
String最慢的原因:
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。以下面一段代码为例:
String str="abc";
System.out.println(str);
str=str+"de";
System.out.println(str);
如果运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
另外,有时候我们会这样对字符串进行赋值:
String str="abc"+"de";
StringBuilder stringBuilder = new StringBuilder().append("abc").append("de");
System.out.println(str);
System.out.println(stringBuilder.toString());
这样输出结果也是“abcde”和“abcde”,但是String的速度却比StringBuilder的反应速度要快很多,这是因为第1行中的操作和
String str="abcde";
是完全一样的,所以会很快,而如果写成下面这种形式:
String str1="abc";
String str2="de";
String str=str1+str2;
那么JVM就会像上面说的那样,不断的创建、回收对象来进行这个操作了。速度就会很慢。
4.2 线程安全方面
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
4.3 总结
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
http://www.newxing.com/regex/
5.1 概念
在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说:
“正则表达式就是记录文本规则的代码。”
可以用于模式匹配和替换的强有力的工具。
可以在几乎所有的基于UNIX系统的工具中找到正则表达式的身影,例如,vi编辑器,Perl或PHP脚本语言,以及awk或sed shell程序等。此外,像JavaScript这种客户端的脚本语言也提供了对正则表达式的支持。
5.2 应用
正则表达式可以让用户通过使用一系列的特殊字符构建匹配模式,然后把匹配模式与数据文件、程序输入以及WEB页面的表单输入等目标对象进行比较,根据比较对象中是否包含匹配模式,执行相应的程序。
最为普遍的应用就是用于验证用户在线输入的邮件地址的格式是否正确。如果通过正则表达式验证用户邮件地址的格式正确,用户所填写的表单信息将会被正常处理;反之,如果用户输入的邮件地址与正则表达的模式不匹配,将会弹出提示信息,要求用户重新输入正确的邮件地址。
5.3 基本语法
正则表达式的形式一般如下:
/love/
位于“/”定界符之间的部分就是将要在目标对象中进行匹配的模式。用户只要把希望查找匹配对象的模式内容放入“/”定界符之间即可。为了能够使用户更加灵活的定制模式内容,正则表达式提供了专门的“元字符”。所谓元字符就是指那些在正则表达式中具有特殊意义的专用字符,可以用来规定其前导字符(即位于元字符前面的字符)在目标对象中的出现模式。
较为常用的元字符包括: “+”, “*”,以及 “?”。其中,“+”元字符规定其前导字符必须在目标对象中连续出现一次或多次,“*”元字符规定其前导字符必须在目标对象中出现零次或连续多次,而“?”元字符规定其前导对象必须在目标对象中连续出现零次或一次。
5.4 元字符
\s:用于匹配单个空格符,包括tab键和换行符;
\S:用于匹配除单个空格符之外的所有字符;
\d:用于匹配从0到9的数字;
\w:用于匹配字母,数字或下划线字符;
\W:用于匹配所有与\w不匹配的字符;
. :用于匹配除换行符之外的所有字符。
(说明:我们可以把\s和\S以及\w和\W看作互为逆运算)
5.5 定位符
较为常用的定位符包括:“^”,“”定位符规定匹配模式必须出现在目标对象的结尾,\b定位符规定匹配模式必须出现在目标字符串的开头或结尾的两个边界之一,而“\B”定位符则规定匹配对象必须位于目标字符串的开头和结尾两个边界之内,即匹配对象既不能作为目标字符串的开头,也不能作为目标字符串的结尾。同样,我们也可以把“^”和“$”以及“\b”和“\B”看作是互为逆运算的两组定位符。举例来说:
/^hell/
因为上述正则表达式中包含“^”定位符,所以可以与目标对象中以 “hell”, “hello”或 “hellhound”开头的字符串相匹配。
/ar$/
因为上述正则表达式中包含“$”定位符,所以可以与目标对象中以 “car”, “bar”或 “ar” 结尾的字符串相匹配。
/\bbom/
因为上述正则表达式模式以“\b”定位符开头,所以可以与目标对象中以 “bomb”, 或 “bom”开头的字符串相匹配。
/man\b/
因为上述正则表达式模式以“\b”定位符结尾,所以可以与目标对象中以 “human”, “woman”或 “man”结尾的字符串相匹配。
为了能够方便用户更加灵活的设定匹配模式,正则表达式允许使用者在匹配模式中指定某一个范围而不局限于具体的字符。例如:
/[A-Z]/
上述正则表达式将会与从A到Z范围内任何一个大写字母相匹配。
/[a-z]/
上述正则表达式将会与从a到z范围内任何一个小写字母相匹配。
/[0-9]/
上述正则表达式将会与从0到9范围内任何一个数字相匹配。
5.6 更多
replace的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换(CharSequence即字符串序列的意思,说白了也就是字符串);
replaceAll的参数是regex,即基于规则表达式的替换,比如:可以通过replaceAll("\\d", "*")把一个字符串所有的数字字符都换成星号;
6.1 相同点の不同点
相同点:都是全部替换,即把源字符串中的某一字符或字符串全部换成指定的字符或字符串;
不同点:replaceAll支持正则表达式,因此会对参数进行解析(两个参数均是),如replaceAll("\\d", "*"),而replace则不会,replace("\\d","*")就是替换"\\d"的字符串,而不会解析为正则。
另外还有一个不同点:“\”在java中是一个转义字符,所以需要用两个代表一个。例如System.out.println( "\\" ) ;只打印出一个"\"。但是“\”也是正则表达式中的转义字符,需要用两个代表一个。所以:\\\\被java转换成\\,\\又被正则表达式转换成\,因此用replaceAll替换“\”为"\\",就要用replaceAll("\\\\","\\\\\\\\"),而replace则replace("\\","\\\\")。
如果只想替换第一次出现的,可以使用replaceFirst(),这个方法也是基于规则表达式的替换,但与replaceAll()不同的是,只替换第一次出现的字符串。
7.1 synchronized的缺陷
synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
7.2 java.util.concurrent.locks包下常用的类
7.2 Lock
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()这四个都是用来获取锁的。
unlock()是用来释放锁的。
7.2.1 Lock
如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try-catch块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
7.2.2 tryLock()
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
使用方法:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
7.2.3 tryLock(long time, TimeUnit unit)
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。使用方法同上
7.2.4 lockInterruptibly()
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
7.2 ReentrantLock
ReentrantLock,意思是“可重入锁”。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock。
//lock()
package com.thread.lock;
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: 98050
* @Time: 2018-12-06 18:03
* @Feature: lock()
*/
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
@Override
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
@Override
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
Lock lock = new ReentrantLock();
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
结果:
Thread-0得到了锁
Thread-1得到了锁
Thread-1释放了锁
Thread-0释放了锁
为什么是这个输出结果?第二个线程怎么会在第一个线程释放锁之前得到了锁?原因在于,在insert方法中的lock变量是局部变量,每个线程执行该方法时都会保存一个副本,那么每个线程执行到lock.lock()处获取的是不同的锁,所以就不会发生冲突。
将lock声明为类的属性即可。
//tryLock()
package com.thread.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: 98050
* @Time: 2018-12-06 18:12
* @Feature: tryLock()的使用
*/
public class Test2 {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Test2 test = new Test2();
new Thread(){
@Override
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
@Override
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
if (lock.tryLock()){
try {
System.out.println(thread.getName()+"得到了锁");
Thread.sleep(3000);
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}else {
System.out.println(thread.getName() + "获取锁失败");
}
}
}
结果:
Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁
//tryLock(long time, TimeUnit unit)
package com.thread.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: 98050
* @Time: 2018-12-06 18:24
* @Feature: tryLock(long time, TimeUnit unit)的使用
*/
public class Test3 {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Test3 test = new Test3();
new Thread(){
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread(){
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
}
public void insert(Thread thread) throws InterruptedException {
if (lock.tryLock(4, TimeUnit.SECONDS)){
try {
System.out.println(thread.getName()+"得到了锁");
Thread.sleep(3000);
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}else {
System.out.println(thread.getName() + "获取锁失败");
}
}
}
结果:
Thread-0得到了锁
Thread-0释放了锁
Thread-1得到了锁
Thread-2释放了锁
//lockInterruptibly()
package com.thread.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: 98050
* @Time: 2018-12-06 19:38
* @Feature: lockInterruptibly()使用方法
*/
public class Test4 {
private Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Test4 test = new Test4();
MyThread thread1 = new MyThread(test);
MyThread thread2 = new MyThread(test);
thread1.start();
thread2.start();
Thread.sleep(2000);
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println(thread.getName()+"得到了锁");
Thread.sleep(4000);
}
finally {
System.out.println(Thread.currentThread().getName()+"执行finally");
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
class MyThread extends Thread {
private Test4 test = null;
public MyThread(Test4 test) {
this.test = test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果,线程2被中断
Thread-0得到了锁
异常代码
Thread-0执行finally
Thread-0释放了锁
https://blog.csdn.net/u012562943/article/details/89706024
乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
https://blog.csdn.net/m47838704/article/details/80013056
1、公平锁能保证:老的线程排队使用锁,新线程仍然排队使用锁。
2、非公平锁保证:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。
/*
* 匿名内部类:没有类名字的内部类(【注意和匿名对象区分】<没有引用指向对象>, 比如:new Stu())
*
* 特点:
* 1:首先得是个局部内部类(其实是特点2决定了,它是局部内部类。为什么?因为创建对(匿名内部类是类的声明以及对象的创建结合一体的写法)象需要执行代码,需要放在方法体中)
* 2:结合类的声明以及实例化为一体。
*3:由于他没有名字,所以需要指定一下它的父类名字,以表明它是哪个类的子类(换句话说,是建立在继承关系上的)
*/
public class NonameInnerClass {
public static void main(String[] args) {
/* //其实匿名内部类就是这个过程的简写
Stu s= new Stu();
s.show();
*/
//既然匿名内部类没有类名,那么还是得要用父类类名(或者是接口名)进行表示一下
new Person(){
//如果比较,重写(覆盖)或实现某些必要的方法
void show() {
// TODO 待续...
System.out.println("匿名内部类的show()");
}
//可以添加自己特有的方法
void self(){
System.out.println("匿名内部类特有的 方法");
}
//我们在匿名内部类中的方法一般不超过三个,方法过多了可读性极差
}.self();
}
}
class Stu extends Person{
void show() {
// TODO 待续...
System.out.println("show()....");
}
}
abstract class Person{
//这个类不一定非要是抽象类,或者是接口,一般的类也行
abstract void show();
}
// 紧凑的格式(写在一行上)
OutputFormat compactFormat = OutputFormat.createCompactFormat();
// 漂亮的格式(自动换行)
OutputFormat prettyFormat = OutputFormat.createPrettyPrint();
//为什么要使用两种格式存储xml文件呢?
//考虑文件的大小;
//传输的话,文件越小越好,所以更需要紧凑型的;
//但是考虑阅读的话,则需要更美观的格式;
12.1 说明
1. 在函数中(说明是局部变量)定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
2. 当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
3. 堆内存用于存放所有由new创建的对象(内容包括该对象其中的所有成员变量)和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。
4. 在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。
1. 局部变量的基本数据类型和引用,存储于栈中,引用的对象实体存储于堆中。因为它们属于方法中的变量,生命周期随方法而结束。
2. 成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体),因为它们属于类,类对象终究是要被new出来使用的。
3. 我们所说的内存泄露,只针对堆内存,他们存放的就是引用指向的对象实体。
//举个例子
public class Sample() {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
/*Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。
mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。*/
- UI(表现层):主要是指与用户交互的界面。用于接收用户输入的数据和显示处理后用户需要的数据。
- BLL:(业务逻辑层):UI层和DAL层之间的桥梁。实现业务逻辑。业务逻辑具体包含:验证、计算、业务规则等等。
- DAL:(数据访问层):与数据库打交道。主要实现对数据的增、删、改、查。将存储在数据库中的数据提交给业务层,同时将业务层处理的数据保存到数据库。(当然这些操作都是基于UI层的。用户的需求反映给界面(UI),UI反映给BLL,BLL反映给DAL,DAL进行数据的操作,操作后再一一返回,直到将用户所需数据反馈给用户)
数据(持久层)访问层 dao UserDao
impl UserDaoImpl
业务逻辑层 service UserService
impl UserServiceImpl
servlet UserInfoServlet
实体类 pojo User
工具类 utils
视图层 JSP HTML SERVLET
userInfo.jsp
- 使用三层架构的目的:解耦
14.1 介绍
MyBatis中使用parameterType向SQL语句传参,parameterType后的类型可以是基本类型int,String,HashMap和java自定义类型。
在SQL中引用这些参数的时候,可以使用两种方式#{parameterName}或者${parameterName}。
在下面的语句中,如果 username 的值为 zhangsan,则两种方式无任何区别:
select * from user t where username = #{username};
select * from user where name = ${username};mith
其解析之后的结果均为:
select * from user where username = 'zhangsan';
但是 #{} 和 ${} 在预编译中的处理是不一样的。#{} 在预处理时,会把参数部分用一个占位符 ? 代替,变成如下的 sql 语句:
select * from user where username = ?;
而 ${} 则只是简单的字符串替换,在动态解析阶段,该 sql 语句会被解析成:
select * from user where username = 'zhangsan';
以上,#{} 的参数替换是发生在 DBMS 中,而 ${} 则发生在动态解析过程中。
14.2 概念
#方式能够很大程度防止sql注入,$方式无法防止Sql注入。
$方式一般用于传入数据库对象,例如传入表名。
从安全性上考虑,能使用#尽量使用#来传参,因为这样可以有效防止SQL注入的问题。
14.3 重点
MyBatis排序时使用order by动态参数时需要注意,用$而不是#!
例如:ORDER BY ${columnName} //这里MyBatis不会修改或转义字符串,可实现动态传入排序。
建议:接受从用户输出的内容并提供给语句中不变的字符串,这样做是不安全的。
这会导致潜在的SQL注入攻击,因此你不应该允许用户输入这些字段,或者通常自行转义并检查。
15.0 原文地址
https://blog.csdn.net/qq30211478/article/details/78016155
15.1 解释
使用ModelAndView类用来存储处理完后的结果数据,以及显示该数据的视图。从名字上看ModelAndView中的Model代表模型,View代表视图,这个名字就很好地解释了该类的作用。业务处理器调用模型层处理完用户请求后,把结果数据存储在该类的model属性中,把要返回的视图信息存储在该类的view属性中,然后让该ModelAndView返回该Spring MVC框架。框架通过调用配置文件中定义的视图解析器,对该对象进行解析,最后把结果数据显示在指定的页面上。
15.2 具体作用
1、返回指定页面
ModelAndView构造方法可以指定返回的页面名称,
也可以通过setViewName()方法跳转到指定的页面 ,
2、返回所需数值
使用addObject()设置需要返回的值,addObject()有几个不同参数的方法,可以默认和指定返回对象的名字。
$ 的作用实际上是字符串拼接,
select * from $tableName$
等效于
StringBuffer sb = new StringBuffer(256);
sb.append("select * from ").append(tableName);
sb.toString();
#用于变量替换
select * from table where id = #id#
等效于
prepareStement = stmt.createPrepareStement("select * from table where id = ?")
prepareStement.setString(1,'abc');
- 拦截器是基于java的反射机制的,而过滤器是基于函数回调。
- 拦截器不依赖与servlet容器,过滤器依赖与servlet容器。
- 拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
- 拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
- 在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
- 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。
- 拦截器可以获取ioc中的service bean实现业务逻辑
过滤器和拦截器触发时机不一样:
过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前。
总结:过滤器包裹住servlet,servlet包裹住拦截器。
https://blog.csdn.net/qq_36411874/article/details/79938439
如何获取客户端ip和url访问中服务器端ip(如果是ip,不是域名)呢?
String requestUrlIP = request.getServerName();
String userIpAddr = request.getRemoteAddr();
System.out.println("***访问的Url中的服务器IP:"+requestUrlIP);
System.out.println("***用户客户端的IP地址:"+userIpAddr);
在开发工作中,我们常常需要获取客户端的IP。一般获取客户端的IP地址的方法是:request.getRemoteAddr();但是在通过了Apache,Squid等反向代理软件就不能获取到客户端的真实IP地址了,用request.getRemoteAddr()方法获取的IP地址是:127.0.0.1或192.168.1.110,而并不是客户端的真实IP。
原因:由于在客户端和服务之间增加了中间代理,因此服务器无法直接拿到客户端的IP,服务器端应用也无法直接通过转发请求的地址返回给客户端。但是在转发请求的HTTP头信息中,增加了X-FORWARDED-FOR信息。用以跟踪原有的客户端IP地址和原来客户端请求的服务器地址。当我们访问index.jsp/时,其实并不是我们浏览器真正访问到了服务器上的index.jsp文件,而是先由代理服务器去访问index.jsp ,代理服务器再将访问到的结果返回给我们的浏览器,因为是代理服务器去访问index.jsp的,所以index.jsp中通过request.getRemoteAddr()的方法获取的IP实际上是代理服务器的地址,并不是客户端的IP地址。
(一)方法一
于是可得出获得客户端真实IP地址的方法一:
public String getRemortIP(HttpServletRequest request) {
if (request.getHeader("x-forwarded-for") == null) {
return request.getRemoteAddr();
}
return request.getHeader("x-forwarded-for");
}
(二)方法二
获得客户端真实IP地址的方法二:
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。如:
X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100
用户真实IP为: 192.168.1.110
https://blog.csdn.net/jdsjlzx/article/details/52675726
每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。
注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。这通常是通过同步那些用来封装列表的对象来实现的。但如果没有这样的对象存在,则该列表需要运用{@link Collections#synchronizedList Collections.synchronizedList}来进行“包装”,该方法最好是在创建列表对象时完成,为了避免对列表进行突发的非同步操作。
下面我们讨论下ArrayList初始默认容量的问题。
有文章说ArrayList默认构造的容量为10,没错。 因为ArrayList的底层是由一个Object[]数组构成的,而这个Object[]数组,默认的长度是10,所以有的文章会说ArrayList长度容量为10。
然而你所指的size()方法,指的是“逻辑”长度。
所谓“逻辑”长度,是指内存已存在的“实际元素的长度” 而“空元素不被计算”
即:当你利用add()方法,向ArrayList内添加一个“元素”时,逻辑长度就增加1位。 而剩下的9个空元素不被计算。
如下代码:
ArrayList<String> list = new ArrayList<String>();
System.out.println("size = " + list.size());
输出结果如下:
size = 0
ArrayList默认size()是0.
19.1 ArrayList源码解析
JDK版本不一样,ArrayList类的源码也不一样。
JDK1.8 ArrayList类结构
/通过ArrayList实现的接口可知,其支持随机访问,能被克隆,支持序列化
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//序列版本号
private static final long serialVersionUID = 8683452581122892189L;
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//被用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//被用于默认大小的空实例的共享数组实例。其与EMPTY_ELEMENTDATA的区别是:当我们向数组中添加第一个元素时,知道数组该扩充多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* Object[]类型的数组,保存了添加到ArrayList中的元素。ArrayList的容量是该Object[]类型数组的长度
* 当第一个元素被添加时,任何空ArrayList中的elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA将会被
* 扩充到DEFAULT_CAPACITY(默认容量)。
*/
transient Object[] elementData; //非private是为了方便嵌套类的访问
// ArrayList的大小(指其所含的元素个数)
private int size;
......
}
ArrayList包含了两个重要的对象:elementData 和 size。
elementData 是”Object[] 类型的数组”,它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建 ArrayList,则elementData的容量默认是10。elementData数组的大小会根据ArrayList容量的增长而动态的增长,具体的增长方式,请参考源码分析中的ensureCapacity()函数。
size 则是动态数组的实际大小。
19.2 为什么ArrayList自动容量扩充选择扩充1.5倍?
这种算法构造出来的新的数组长度的增量都会比上一次大( 而且是越来越大) ,即认为客户需要增加的数据很多,而避免频繁newInstance 的情况。
ArrayList的writeObject方法就会显式的为每个实际的数组元素进行序列化,只序列化有用的元素。
https://www.cnblogs.com/zjfjava/p/9897650.html
List去重的问题,除了遍历去重,我们常常想到利用Set集合不允许重复元素的特点,通过List和Set互转,来去掉重复元素。
https://www.cnblogs.com/hamawep789/p/10840774.html
21.1 springmvc工作流程
1、用户向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet(也叫中央控制器)。
2、DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller来处理(并未调用Controller,只是得知)
3、DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller
4、HandlerAdapter处理器适配器去执行Controller并得到ModelAndView(数据和视图),并层层返回给DispatcherServlet
5、DispatcherServlet将ModelAndView交给ViewReslover视图解析器解析,然后返回真正的视图。
6、DispatcherServlet将模型数据填充到视图中
7、DispatcherServlet将结果响应给用户
21.2 组件说明
- 1.DispatcherServlet:前端控制器,也称为中央控制器,它是整个请求响应的控制中心,组件的调用由它统一调度。
- 2.HandlerMapping:处理器映射器,它根据用户访问的 URL 映射到对应的后端处理器 Handler。也就是说它知道处理用户请求的后端处理器,但是它并不执行后端处理器,而是将处理器告诉给中央处理器。
- 3.HandlerAdapter:处理器适配器,它调用后端处理器中的方法,返回逻辑视图 ModelAndView 对象。
- 4.ViewResolver:视图解析器,将 ModelAndView 逻辑视图解析为具体的视图(如 JSP)。
- 5.Handler:后端处理器,对用户具体请求进行处理,也就是我们编写的 Controller 类。
https://blog.csdn.net/zhanglf02/article/details/78132304
22.1 AOP的基本概念
切面(Aspect) :通知(advice)和切入点(pointcut)共同组成了切面(aspect),时间、地点和要发生的“故事”。可以从注解方式来理解,代码如下
@aspect为类上面的注解——切面
@pointcut(…)——切入点。为此类内一个空方法上面的注解。可以把拦截的地址表达式表示为方法签名,利于使用起来方便。
@before@after等——通知。为此类下面的方法上面的注解。
三者在一块组成一个切面。
@Aspect
public class ExampleAspect {
@Pointcut("execution(* com.psjay.example.spring.aop.*.*(..))")
public void aPointcut() {
}
@Before("aPointcut()")
public void beforeAdvice() {
System.out.println("before advice is executed!");
}
}
连接点(Joinpoint) :程序能够应用通知的一个“时机”,这些“时机”就是连接点,例如方法被调用时、异常被抛出时等等。——可以理解为被aop拦截的类或者方法就是连接点。
通知(Advice) :通知定义了切面是什么以及何时使用。描述了切面要完成的工作和何时需要执行这个工作。——可以理解为被注解有@Before等advice注解的安全校验的方法,拦截了过来的请求要做什么逻辑的校验。
切入点(Pointcut) :通知定义了切面要发生的“故事”和时间,那么切入点就定义了“故事”发生的地点,例如某个类或方法的名称。——可以理解为切面切向哪里?是个类或者某层的包路径。
目标对象(Target Object) :即被通知的对象。
AOP代理(AOP Proxy) 在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。默认情况下,TargetObject实现了接口时,则采用JDK动态代理;反之,采用CGLIB代理。
织入(Weaving)把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
(1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才能做到,例如AspectJ的织入编译器;
(2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码;
(3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理是使用了JDK的动态代理。
2.2 通知(Advice)类型的说明@Before 前置通知(Before advice)
- @After 后通知(After advice)
- @AfterReturning 返回后通知(After return advice)
- @Around 环绕通知(Around advice)
- @AfterThrowing 抛出异常后通知(After throwing advice)
22.3 advice(通知)注解的执行先后顺序
执行到核心业务方法或者类时,会先执行AOP。在aop的逻辑内,先走@Around注解的方法。然后是@Before注解的方法,然后这两个都通过了,走核心代码,核心代码走完,无论核心有没有返回值,都会走@After方法。然后如果程序无异常,正常返回就走@AfterReturn,有异常就走@AfterThrowing。
22.4 在aop中校验不通过如何不让程序进入核心代码?
通过aop中注解的执行的先后顺序我们知道,校验发生在核心代码前面的只剩下两个——@Before,@Around。
@Before : 这个注解只有在异常时才不会走核心方法——连接点。正常@Before无法阻止当前线程进入连接点。
@Around : 这个注解在连接点前后执行。并且注解的方法传入的ProceedingJionPoint 类中封装的代理方法proceed()可以让当前线程从aop方法转到连接点——核心代码方法。所以一般我们用这个注解,如果aop的安全校验不通过,则不调用proceed()方法,就永远不会进入连接点。
除此外,要注意除了Around注解的方法可以传ProceedingJionPoint 外,别的几个都不能传这个类。但是普通的数据类型是不限制的。注解的方法的返回值也不限制,可以自由限制。
https://blog.csdn.net/lvhaoguang0/article/details/80874536
当我们有多条sql语句需要发送到数据库执行的时候,有两种发送方式,一种是执行一条发送一条sql语句给数据库,另一个种是发送一个sql集合给数据库,也就是发送一个批sql到数据库。
很显然两者的数据库执行效率是不同的,我们发送批处理sql的时候数据库执行效率要高。所以我们有必要掌握mysql数据库的sql批处理发送方式方法。
https://www.cnblogs.com/hggen/p/6264475.html
24.1 Tomcat:
Tomcat是一个JSP/Servlet容器。其作为Servlet容器,有三种工作模式:独立的Servlet容器、进程内的Servlet容器和进程外的Servlet容器。
24.2 Tomcat请求过程:
- 1、用户点击网页内容,请求被发送到本机端口8080,被在那里监听的Coyote HTTP/1.1 Connector获得。
- 2、Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应。
- 3、Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
- 4、Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context去处理)。
- 5、path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类。
- 6、构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost().执行业务逻辑、数据存储等程序。
- 7、Context把执行完之后的HttpServletResponse对象返回给Host。
- 8、Host把HttpServletResponse对象返回给Engine。
- 9、Engine把HttpServletResponse对象返回Connector。
- 10、Connector把HttpServletResponse对象返回给客户Browser。
https://blog.csdn.net/belalds/article/details/81106887
25.1 一级缓存
- 一级缓存基于sqlSession默认开启,在操作数据库时需要构造SqlSession对象,在对象中有一个HashMap用于存储缓存数据。不同的SqlSession之间的缓存数据区域是互相不影响的。
- 一级缓存的作用域是SqlSession范围的,当在同一个sqlSession中执行两次相同的sql语句时,第一次执行完毕会将数据库中查询的数据写到缓存(内存), 第二次查询时会从缓存中获取数据,不再去底层数据库查询,从而提高查询效率。
- 需要注意的是,如果SqlSession执行了DML操作(增删改),并且提交到数据库,MyBatis则会清空SqlSession中的一级缓存,这样做的目的是为了保证缓存中存储的是最新的信息,避免出现脏读现象。
- 当一个SqlSession结束后该SqlSession中的一级缓存也就不存在了。
- 关闭一级缓存后,再次访问,需要再次获取一级缓存,然后才能查找数据,否则会抛出异常。
- 一级缓存的范围有SESSION和STATEMENT两种,默认是SESSION,如果不想使用一级缓存,可以把一级缓存的范围指定为STATEMENT,这样每次执行完一个Mapper中的语句后都会将一级缓存清除。
- 如果需要更改一级缓存的范围,可以在Mybatis的配置文件中,在下通过localCacheScope指定。
<setting name="localCacheScope" value="STATEMENT"/>
- 需要注意的是,当Mybatis整合Spring后,直接通过Spring注入Mapper的形式,如果不是在同一个事务中每个Mapper的每次查询操作都对应一个全新的SqlSession实例,这个时候就不会有一级缓存的命中,但是在同一个事务中时共用的是同一个SqlSession。
如有需要可以启用二级缓存。
25.2 二级缓存
- Mybatis的二级缓存是指mapper映射文件。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。Mybatis需要手动设置启动二级缓存。
二级缓存是默认启用的(要生效需要对每个Mapper进行配置),如想取消,则可以通过Mybatis配置文件中的元素下的子元素来指定cacheEnabled为false。
<settings>
<setting name="cacheEnabled" value="false" />
</settings>
- 二级缓存是mapper级别的缓存。使用二级缓存时,多个SqlSession使用同一个Mapper的sql语句去操作数据库,得到的数据会存在二级缓存区域,它同样是使用HashMap进行数据存储。相比一级缓存SqlSession,二级缓存的范围更大,多个Sqlsession可以共用二级缓存,二级缓存是跨SqlSession的。
- 二级缓存的作用域是mapper的同一个namespace。不同的sqlSession两次执行相同的namespace下的sql语句,且向sql中传递的参数也相同,即最终执行相同的sql语句,则第一次执行完毕会将数据库中查询的数据写到缓存,第二次查询会从缓存中获取数据,不再去底层数据库查询,从而提高效率。
在MyBatis配置文件(mybatis-config.xml)中开启二级缓存(详细过程自己百度搜索开启)
//value属性默认为false在**Mapper.xml中开启当前mapper的namespace下的二级缓存,代表创建了一个LRU缓存,并每隔60秒刷新,最大存储512个对象,而且返回的对象被认为是只读的。
- evicition收回策略,默认是LRU
(1)LRU最近最少使用策略,一处做长时间不被使用的对象。
(2)FIFO先进先出策略,按对象进入缓存的顺序来移除它们。
(3)SOFT软引用策略,移除基于垃圾回收器状态和软引用规则的对象。
(4)WEAK弱引用策略,更积极地移除基于垃圾收集器状态和弱引用规则的对象
https://blog.csdn.net/qq_37774171/article/details/85495018
- MyBatis因为具有封装少,映射多样化,支持存储过程,可以进行SQL优化等特点。使得它取代了Hibernate成为了java互联网中首选的持久框架。
- 无论MyBatis或Hibernate都可以称为ORM框架,Hibernate的设计理念是完全面向POJO的,而MyBatis不是。
- Hibernate基本不再需要编写SQL就可以通过映射关系来操作数据库,是一种全表映射的体现,而MyBatis需要我们提供SQL去运行。程序员不用精通SQL,只要懂得操作POJO就能够操作对应数据库的表。
- 在管理系统时代,首先是实现业务逻辑,然后才是性能,所以Hibernate在当时是主流。
- 在移动互联网时代,MyBatis是首选,不屏蔽SQL,程序员可以自己制定SQL规则,能更加精确定义SQL,从而优化性能。更符合移动互联网高并发,大数据,高性能,高响应的要求。
- Hibernate和MyBatis的增、删、查、改.对于业务逻辑层来说大同小异,对于映射层而言Hibernate的配置不需要接口和SQL.相反MyBatis是需要的。对于Hibernate而言,不需要编写大量的SQL,就可以完全映射,同时提供了日志、缓存、级联(级联比MyBatis强大)等特性,此外还提供HQL (Hibernate Query Language)对POIO进行操作,使用十分方便,但是它也有致命的缺陷。
- 由于无须SQL,当多表关联超过3个的时候,通过Hibernate的级联会造成太多性能的丢失,又或者我现在访问一个财 务的表,然后它会关联财产信息表,财产又分为机械、原料等.显然机械和原料的字段是不一样的,这样关联字段只能根据特定的条件 变化而变化,而Hibernate无法支持这样的变化。遇到存储过程,Hibernate只能作罢。更为关键的是性能,在管理系统的时代,对于性能的要求不是那么苛刻,但是在互联网时代性能就是系统的根本,响应过慢就会丧失客户,试想一下谁会去用一个经常需要等待超过10 秒以上的应用呢?
- 以上的问题MyBatis都可以解决,MyBatis 可以自由书写SQL、支持动态SQL、处理列表、动态生成表名,支持存储过程。这样就可以灵活地定义查询语句,满足各类需求和性能优化的需要,这些在互联网系统中是十分重要的。
- 但MyBatis也有缺陷。首先,它要编写SQL和映射规则,其工作量稍微大于 Hibernate. 其次,它支持的工具也很有限,不能像Hibernate那样有许多的插件可以帮助生成映射代码和关联关系,而即使使用生成工具,往往也需要开发者进一步简化, MyBatis 通过手工编码,工作量相对大些。所以对于性能要求不太苛刻的系统,比如管理系统、ERP 等推荐使用Hibernate;而对于性能要求高、响应快、灵活的系统则推荐使用MyBatis.
https://blog.csdn.net/csdn1428208016/article/details/85243068
if
choose, when, otherwise
trim, where, set
foreach
bind
https://www.cnblogs.com/xiepeixing/p/4243288.html
28.1 简介
- 在SpringMVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model 返回给对应的View 进行展示。
- 在SpringMVC 中提供了一个非常简便的定义Controller 的方法,你无需继承特定的类或实现特定的接口,只需使用@Controller 标记一个类是Controller ,然后使用@RequestMapping 和@RequestParam 等一些注解用以定义URL 请求和Controller 方法之间的映射,这样的Controller 就能被外界访问到。
- 此外Controller 不会直接依赖于HttpServletRequest 和HttpServletResponse 等HttpServlet 对象,它们可以通过Controller 的方法参数灵活的获取到。
28.2 使用 @Controller 定义一个 Controller 控制器
- @Controller 用于标记在一个类上,使用它标记的类就是一个SpringMVC Controller 对象。分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping 注解。@Controller 只是定义了一个控制器类,而使用@RequestMapping 注解的方法才是真正处理请求的处理器, 单单使用@Controller 标记在一个类上还不能真正意义上的说它就是SpringMVC 的一个控制器类,因为这个时候Spring 还不认识它。
28.3 使用 @RequestMapping 来映射 Request 请求与处理器
- 可以使用@RequestMapping 来映射URL 到控制器类,或者是到Controller 控制器的处理方法上。当@RequestMapping 标记在Controller 类上的时候,里面使用@RequestMapping 标记的方法的请求地址都是相对于类上的@RequestMapping 而言的;当Controller 类上没有标记@RequestMapping 注解时,方法上的@RequestMapping 都是绝对路径。这种绝对路径和相对路径所组合成的最终路径都是相对于根路径“/ ”而言的。
https://blog.csdn.net/rocling/article/details/82350515
https://blog.csdn.net/weixin_42617262/article/details/85344819
- 在java中,原则上是不允许多继承的,也就是类与类之间只可以单继承。
- 第一种使用内部类(成员内部类)就可以多继承,严格来说,还不是实现多继承,但是这种方法可以实现多继承所需的功能,所以把它称为实现了多继承。
- 第二种如果要直接继承类,子类不可直接多继承,但可通过多层继承实现多继承。但多层继承一般建议不超过三次,且代码较冗余。
- 第三种实现多继承的方法是接口,在同时可用内部类和接口时,优先使用接口。因内部类需要应用于继承关系,接口可用于继承也可用于其他,比较灵活。
https://blog.csdn.net/rocling/article/details/102791048
30.1 :面试题
- 为什么要分库分表?
- 用过哪些分库分表中间件?
- 不同的分库分表中间件都有什么优点和缺点?
- 你们具体是如何对数据库如何进行垂直拆分或水平拆分的?
30.2 面试官心理分析
其实这块肯定是扯到高并发了,因为分库分表一定是为了支撑高并发、数据量大两个问题的。而且现在说实话,尤其是互联网类的公司面试,基本上都会来这么一下,分库分表如此普遍的技术问题,不问实在是不行,而如果你不知道那也实在是说不过去!
30.3 面试题剖析
分表
分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。
分库
分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。
# | 分库分表前 | 分库分表后 |
---|---|---|
并发支撑情况 | MySQL单机部署,扛不住高并发 | MySQL从单机到多机,能承受的并发增加了多倍 |
磁盘使用情况 | MySQL单机磁盘容量几乎撑满 | 拆分为多个库,数据库服务器磁盘使用率大大降低 |
SQL执行性能 | 单表数据量太大,SQL越跑越慢 | 单表数据量减少,SQL执行效率明显上升 |
30.3.2 分库和分表用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?
- 比较常见的包括:
- cobar
阿里 b2b 团队开发和开源的,属于 proxy 层方案,就是介于应用服务器和数据库服务器之间。应用程序通过 JDBC 驱动访问 cobar 集群,cobar 根据 SQL 和分库规则对 SQL 做分解,然后分发到 MySQL 集群不同的数据库实例上执行。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。- TDDL
淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。- atlas
360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。- sharding-jdbc
当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案。- mycat
基于 cobar 改造的,属于 proxy 层(代理服务器)方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。
31.1 事务概念
事务是由一步或几步数据库操作序列组成逻辑执行单元,这系列操作要么全部执行,要么全部放弃执行。
31.2. 事务的基本要素(ACID)
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
31.3 事务的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
31.3 MySQL事务隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
读已提交(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
mysql默认的事务隔离级别为repeatable-read
https://blog.csdn.net/zcl_love_wx/article/details/83305645
- mysql的锁类型
- (1) 共享/排它锁(Shared and Exclusive Locks)
共享锁和排他锁是InnoDB引擎实现的标准行级别锁。
拿共享锁是为了让当前事务去读一行数据。
拿排他锁是为了让当前事务去修改或删除某一行数据。。
设置共享锁:select * from user where id = 1 LOCK IN SHARE MODE;
设置排他锁:select * from user where id = 1 FOR UPDATE;- (2) 意向锁(Intention Locks)
意向锁存在的意义在于,使得行锁和表锁能够共存。
意向锁是表级别的锁,用来说明事务稍后会对表中的数据行加哪种类型的锁(共享锁或独占锁)。
当一个事务对表加了意向排他锁时,另外一个事务在加锁前就会通过该表的意向排他锁知道前面已经有事务在对该表进行独占操作,从而等待。- (3) 记录锁(Record Locks)
记录锁是索引记录上的锁,例如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;会阻止其他事务对c1=10的数据行进行插入、更新、删除等操作。
记录锁总是锁定索引记录。如果一个表没有定义索引,那么就会去锁定隐式的“聚集索引”。- (4) 间隙锁(Gap Locks)
间隙锁是一个在索引记录之间的间隙上的锁。
一个间隙可能跨越单个索引值、多个索引值,甚至为空。
对于使用唯一索引 来搜索唯一行的语句,只加记录锁不加间隙锁(这并不包括组合唯一索引)。- (5) 临键锁(Next-key Locks)
Next-Key Locks是行锁与间隙锁的组合。当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上记录锁(Record Lock),然后再对索引记录两边的间隙加上间隙锁(Gap Lock)。- (6) 插入意向锁(Insert Intention Locks)
插入意向锁是在数据行插入之前通过插入操作设置的间隙锁定类型。
如果多个事务插入到相同的索引间隙中,如果它们不在间隙中的相同位置插入,则无需等待其他事务。例如:在4和7的索引间隙之间两个事务分别插入5和6,则两个事务不会发冲突阻塞。- (7) 自增锁(Auto-inc Locks)
自增锁是事务插入到有自增列的表中而获得的一种特殊的表级锁。如果一个事务正在向表中插入值,那么任何其他事务都必须等待,保证第一个事务插入的行是连续的自增值。
https://www.cnblogs.com/kevingrace/p/9004460.html
- Redis的集群方案大致有三种:
1)redis cluster集群方案;
2)master/slave主从方案;
3)哨兵模式来进行主从替换以及故障恢复。- Sentinel(哨兵)是用于监控redis集群中Master状态的工具,是Redis 的高可用性解决方案,sentinel哨兵模式已经被集成在redis2.4之后的版本中。sentinel是redis高可用的解决方案,sentinel系统可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。
- sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,集群中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。
- 就Redis和MongoDB来说,大家一般称之为Redis缓存、MongoDB数据库。这也是有道有理有根据的,Redis主要把数据存储在内存中,其“缓存”的性质远大于其“数据存储“的性质,其中数据的增删改查也只是像变量操作一样简单;MongoDB却是一个“存储数据”的系统,增删改查可以添加很多条件,就像SQL数据库一样灵活,这一点在面试的时候很受用。
- MongoDB和Redis都是NoSQL,采用结构型数据存储。二者在使用场景中,存在一定的区别,这也主要由于
二者在内存映射的处理过程,持久化的处理方法不同。MongoDB建议集群部署,更多的考虑到集群方案,Redis 更偏重于进程顺序写入,虽然支持集群,也仅限于主-从模式。
指标 | MongoDB(v2.4.9) | Redis(v2.4.17) | 比较说明 |
---|---|---|---|
比较说明 | C++ | C/C++ | - |
协议 | BSON、自定义二进制 | 类Telnet | - |
性能 | 依赖内存,TPS(吞吐量)较高 | 依赖内存,TPS非常高 | Redis优于MongoDB |
可操作性 | 丰富的数据表达、索引;最类似于关系数据库,支持丰富的查询语言 | 数据丰富,较少的IO | MongoDB优于Redis |
内存及存储 | 适合大数据量存储,依赖系统虚拟内存管理,采用镜像文件存储;内存占有率比较高,官方建议独立部署在64位系统(32位有最大2.5G文件限制,64位没有改限制) | Redis2.0后增加虚拟内存特性,突破物理内存限制;数据可以设置时效性,类似于memcache | 不同的应用角度看,各有优势 |
可用性 | 支持master-slave,replicaset(内部采用paxos选举算法,自动故障恢复),auto sharding机制,对客户端屏蔽了故障转移和切分机制 | 依赖客户端来实现分布式读写;主从复制时,每次从节点重新连接主节点都要依赖整个快照,无增量复制;不支持自动sharding,需要依赖程序设定一致hash机制 | MongoDB优于Redis;单点问题上,MongoDB应用简单,相对用户透明,Redis比较复杂,需要客户端主动解决。(MongoDB 一般会使用replica sets和sharding功能结合,replica sets侧重高可用性及高可靠性,而sharding侧重于性能、易扩展) |
可靠性 | 从1.8版本后,采用binlog方式(MySQL同样采用该方式)支持持久化,增加可靠性 | 依赖快照进行持久化;AOF增强可靠性;增强可靠性的同时,影响访问性能 | MongoDB优于Redis |
一致性 | 不支持事物,靠客户端自身保证 | 支持事物,比较弱,仅能保证事物中的操作按顺序执行 | Redis优于MongoDB |
数据分析 | 内置数据分析功能(mapreduce) | 不支持 | MongoDB优于Redis |
应用场景 | 海量数据的访问效率提升 | 较小数据量的性能及运算 | MongoDB优于Redis |
https://www.jianshu.com/p/7a5b0043b035
举个最简单点的例子来区分 面向过程和面向对象
有一天你想吃鱼香肉丝了,怎么办呢?你有两个选择
1、自己买材料,肉,鱼香肉丝调料,蒜苔,胡萝卜等等然后切菜切肉,开炒,盛到盘子里。
2、去饭店,张开嘴:老板!来一份鱼香肉丝!
看出来区别了吗?这就是1是面向过程,2是面向对象。
面向对象有什么优势呢?首先你不需要知道鱼香肉丝是怎么做的,降低了耦合性。如果你突然不想吃鱼香肉丝了,想吃洛阳白菜,对于1你可能不太容易了,还需要重新买菜,买调料什么的。对于2,太容易了,大喊:老板!那个鱼香肉丝换成洛阳白菜吧,提高了可维护性。总的来说就是降低耦合,提高维护性!
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。
面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们我们使用的就是面向对象了。
- 面向过程:
优点:性能比面向对象好,因为类调用时需要实例化,开销比较大,比较消耗资源。
缺点:不易维护、不易复用、不易扩展.- 面向过程:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 .
缺点:性能比面向过程差
35.1 面向对象的三大特性:
1、封装
隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
2、继承
提高代码复用性;继承是多态的前提。
3、多态
父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
35.2 五大基本原则:
1、单一职责原则SRP(Single Responsibility Principle)
类的功能要单一,不能包罗万象,跟杂货铺似的。
2、开放封闭原则OCP(Open-Close Principle)
一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。
3、里式替换原则LSP(the Liskov Substitution Principle LSP)
子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~
4、依赖倒置原则DIP(the Dependency Inversion Principle DIP)
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的是抽象的中国人,而不是你是xx村的。
5、接口分离原则ISP(the Interface Segregation Principle ISP)
设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。
https://blog.csdn.net/xuhuaabc/article/details/91475761
- 一、HashMap简介
HashMap是在JDK1.2中引入的Map的实现类。- 1.HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
- 2.HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。
- 3.HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。
- 4.HashMap存数据的过程是:
HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。- 5.HashMap中key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。
- 哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。
- HashMap内存储数据的Entry数组默认是16,(默认情况下,hashmap大小为16,即1<<4就是1乘以2的4次幂=16),如果没有对Entry扩容机制的话,当存储的数据一多,Entry内部的链表会很长,这就失去了HashMap的存储意义了。所以HasnMap内部有自己的扩容机制。
- HashMap内部有:
- 变量size,它记录HashMap的底层数组中已用槽的数量;
- 变量threshold,它是HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
- 变量DEFAULT_LOAD_FACTOR = 0.75f,默认加载因子为0.75
- HashMap扩容的条件是:当size大于threshold时,对HashMap进行扩容
- 扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将旧的HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。
- HashMap共有四个构造方法。构造方法中提到了两个很重要的参数:初始容量和加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中槽的数量(即哈希数组的长度),初始容量是创建哈希表时的容量(从构造函数中可以看出,如果不指明,则默认为16),加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即扩容)。
- 下面说下加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。
- 另外,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方。
- 二、Hashtable简介
- 1.Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
- 2.Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。
- 3.Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。
- 三.分析二者不同
- 1.继承的父类不同
HashMap继承自AbstractMap类。但二者都实现了Map接口。
Hashtable继承自Dictionary类,Dictionary类是一个已经被废弃的类(见其源码中的注释)。父类都被废弃,自然而然也没人用它的子类Hashtable了。- 2.HashMap线程不安全,HashTable线程安全
javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
Hashtable 中的方法大多是Synchronize的,而HashMap中的方法在一般情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。HashTable实现线程安全的代价就是效率变低,因为会锁住整个HashTable,而ConcurrentHashMap做了相关优化,因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定,效率比HashTable高很多。
HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
HashMap的put方法:
void addEntry(int hash, K key, V value, int bucketIndex) { //新增Entry,将“key-value”插入指定位置,bucketIndex是位置索引。
Entry<K,V> e = table[bucketIndex]; //// 保存“bucketIndex”位置的值到“e”中
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold) // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小2倍
resize(2 * table.length);
}
- 在hashmap的put方法调用addEntry()方法,假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。
- 故解决方法就是使用 使用ConcurrentHashMap。
- 这里要说一下 就是HashMap的迭代器(Iterator)是fail-fast迭代器,故当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException异常,而Hashtable的enumerator迭代器不是fail-fast的。但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
- 3.包含的contains方法不同
HashMap是没有contains方法的,而包括containsValue和containsKey方法;hashtable则保留了contains方法,效果同containsValue,还包括containsValue和containsKey方法。- 4.是否允许null值
Hashmap是允许key和value为null值的,用containsValue和containsKey方法判断是否包含对应键值对;
HashTable键值对都不能为空,否则包空指针异常。- 5.计算hash值方式不同
为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。
①:HashMap有个hash方法重新计算了key的hash值,因为hash冲突变高,所以通过一种方法重算hash值的方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//注意这里计算hash值,先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值。
②:Hashtable通过计算key的hashCode()**来得到hash值就为最终hash值。
它们计算索引位置方法不同:
1. HashMap在求hash值对应的位置索引时,index = (n - 1) & hash。将哈希表的大小固定为了2的幂,因为是取模得到索引值,故这样取模时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
2. HashTable在求hash值位置索引时计算index的方法:
int index = (hash & 0x7FFFFFFF) % tab.length;
//&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号位改变,而后面的位都不变。
6.扩容方式不同(容量不够)
当容量不足时要进行resize方法,而resize的两个步骤:
①扩容;
②rehash:这里HashMap和HashTable都会会重新计算hash值而这里的计算方式就不同了(看5);
HashMap 哈希扩容必须要求为原容量的2倍,而且一定是2的幂次倍扩容结果,而且每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入;
而Hashtable扩容为原容量2倍加1;7.解决hash冲突方式不同(地址冲突)
查找时间复杂度慢慢变高;
先看jdk8之前:
链地址法:将所有关键字为同义词的记录存储在同一线性表中。即在Hash出来的哈希地址中不直接存Key,而是存储一个Key的链表,当发生冲突时,将同义的Key加入链表。
Java8,HashMap中,当出现冲突时可以:
1.如果冲突数量小于8,则是以链表方式解决冲突。
2.而当冲突大于等于8时,就会将冲突的Entry转换为红黑树进行存储。
3.而又当数量小于6时,则又转化为链表存储。
而在HashTable中, 都是以链表方式存储。
https://www.jianshu.com/p/7d953fe064ed
- 对于left join
- 在使用left join时,on and 和on where会有区别
- 1.on的条件是在连接生成临时表时使用的条件,以左表为基准 ,不管on中的条件真否,都会返回左表中的记录
- 2.where条件是在临时表生成好后,再对临时表过滤。此时和left join有区别(返回左表全部记录),条件不为真就全部过滤掉,on后的条件来生成左右表关联的临时表,where后的条件是生成临时表后对临时表过滤
- 对于inner join
- 在使用inner join时,on and 和on where没有区别
- 在使用inner join时,不管是对左表还是右表进行筛选,on and和on where都会对生成的临时表进行过滤
总结:
on and是进行韦恩运算时 连接时就做的动作,where是全部连接完后,再根据条件过滤
https://www.cnblogs.com/jasontec/p/9699242.html
- String(字符串)
格式: set key value
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个键最大能存储512MB。- list(双向链表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
格式: lpush name value
在 key 对应 list 的头部添加字符串元素
格式: rpush name value
在 key 对应 list 的尾部添加字符串元素
格式: lrem name index
key 对应 list 中删除 count 个和 value 相同的元素
格式: llen name
返回 key 对应 list 的长度- set(hash表)
格式: sadd name value
Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。- zset(排序set)
zset是set的一个升级版本,他在set的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,zset会自动重新按新的值调整顺序。 可以对指定键的值进行排序权重的设定,它应用排名模块比较多格式: zadd name score value
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。- Hash类型
格式: hmset name key1 value1 key2 value2
Redis hash 是一个键值(key=>value)对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。Redis能够存储key对多个属性的数据(比如user1.uname user1.passwd),当然,你完成可以把这些属性以json格式进行存储,直接把它当作string类型进行操作,但这样性能上是对影响的,所以redis提出的Hash类型。- 增加操作:String(set、mset),List(lpush 、rpush),Set(sadd),Hash(hset、hmset),Zset(zadd)
- 删除操作:String(del),List(lpop、rpop),Set(srem,spop),Hash(Hdel),Zset(zrem)
- 查询操作:String(get),List(lrange),Set(smembers),Hash(hkeys,hvals,hgetall),Zset(zrange)
- 返回长度操作:List(llen),Set(scard),Hash(hlen),Zset(zcard)
https://blog.csdn.net/ocean181/article/details/6720371
39.1 什么是面向对象编程(Object-Oriented Programming)?
面向对象编程(Object-Oriented Programming)简称OOP技术,是开发计算机应用程序的一种新方法、新思想。过去的面向过程编程常常会导致所有的代码都包含在几个模块中,使程序难以阅读和维护。在做一些修改时常常牵一动百,使以后的开发和维护难以为继。
而使用OOP技术,常常要使用许多代码模块,每个模块都只提供特定的功能,它们是彼此独立的,这样就增大了代码重用的几率,更加有利于软件的开发、维护和升级。
在面向对象中,算法与数据结构被看做是一个整体,称作对象,现实世界中任何类的对象都具有一定的属性和操作,也总能用数据结构与算法两者合一地来描述,所以可以用下面的等式来定义对象和程序:
对象=(算法+数据结构),程序=(对象+对象+……)。
从上面的等式可以看出,程序就是许多对象在计算机中相继表现自己,而对象则是一个个程序实体。
39.2 什么是面向组件编程(Component-Oriented Programming)?
组件不是一个新的概念,Java中的javaBean规范和EJB规范都是典型的组件。组件的特点在于他定义了一种通用的处理方式。例如,JavaBean 拥有内视的特性,这样就可以通过工具来实现JavaBean的可视化。而EJB规范定义了企业服务中的一些特性,使得EJB容器能够为符合EJB规范的代码增添企业计算所需要的能力,例如事务、持久化、池等。
所以,组件比起对象来的进步就在于通用的规范的引入。通用规范往往能够为组件添加新的能力(就像上面所讨论的),但也给组件添加了限制,例如你需要实现EJB的一些接口
COP比OOP更进一步。通常OOP将数据对象组织到实体中。这种方法具有很多优点。但是,OOP有一个大的限制:对象之间的相互依赖关系。去掉这个限制的一个好的想法就是组件。组件和一般对象之间的关键区别是组件是可以替代的。
39.3 什么是面向方面编程(Aspect-Oriented Programming)?
将通用需求功能从不相关类之中分离出来;同时,能够使得很多类共享一个行为,一旦行为发生变化,不必修改很多类,只要修改这个行为就可以。
AOP就是这种实现分散关注的编程方法,它将“关注”封装在“方面”中。
39.4 什么是面向服务编程(Service-Oriented Programming)?
SOP是一种体系结构,目标是在软件代理交互中获得松散耦合。一个服务是一个服务提供者为一个服务消费者获得其想要的最终结果的一个工作单元。服务者与消费者都以软件代理代表他们自己的角色。
这听起来有些太抽象,但是SOP确实无处不在。让我们在你的住房中找到一个SOP的例子。例如播放一个CD,你可以将要播放的CD放入CD机中,CD机将为你播放这张CD,CD机提供了一个CD播放服务。这里的好处就是你可以用不同的CD机去播放同一张CD。他们能提供同样的CD播放服务,但是服务质量是不同的。
SOP的思想明显不同于面向对象的编程,面向对象编程强烈的建议你应该将数据与其操作绑定。因此在面向对象编程风格中,每张CD有它自己的CD播放机,他们之间不能被拆开。这听起来很奇怪,但是这就是我们建立许多已存软件系统的方式。
而SOP就不一样了,为了减少异构性、互操作性和不断改变的要求的问题,这样的体系结构应该提供平台来构建具有下列特征的应用程序服务:
松散耦合、位置透明、协议独立
基于这样的面向服务的体系结构,服务使用者甚至不必关心与之通信的特定服务,因为底层基础设施或服务“总线”将代表使用者做出适当的选择。基础设施对请求者隐藏了尽可能多的技术。特别地,来自不同实现技术(如 J2EE 或 .NET)的技术规范不应该影响 SOP用户。如果已经存在一个服务实现,我们就还应该重新考虑用一个“更好”的服务实现来代替,新的服务实现必须具有更好的服务质量。
OOA:Object Oriented Analysis 面向对象分析方法
OOD:Object Oriented Design 面向对象设计
OOP:Object Oriented Programming 面向对象的程序设计
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
Redis 提供了两种持久化方式:RDB(默认) 和AOF
存储结构:
内容是redis通讯协议(RESP )格式的命令文本存储。
比较:
1、aof文件比rdb更新频率高,优先使用aof还原数据。
2、aof比rdb更安全也更大
3、rdb性能比aof好
4、如果两个都配了优先加载AOF
Redis 即 REmote Dictionary Server (远程字典服务);
而Redis的协议规范是 Redis Serialization Protocol (Redis序列化协议)
该协议是用于与Redis服务器通信的,用的较多的是Redis-cli通过pipe与Redis服务器联系;
协议如下:
客户端以规定格式的形式发送命令给服务器;
服务器在执行最后一条命令后,返回结果。
- 客户端发送命令的格式(类型):5种类型
间隔符号,在Linux下是\r\n,在Windows下是\n- 1.简单字符串 Simple Strings, 以 "+"加号 开头
格式:+ 字符串 \r\n
字符串不能包含 CR或者 LF(不允许换行)
eg: "+OK\r\n"
注意:为了发送二进制安全的字符串,一般推荐使用后面的 Bulk Strings类型- 2.错误 Errors, 以"-"减号 开头
格式:- 错误前缀 错误信息 \r\n
错误信息不能包含 CR或者 LF(不允许换行),Errors与Simple Strings很相似,不同的是Erros会被当作异常来看待
eg: "-Error unknow command 'foobar'\r\n"- 3.整数型 Integer, 以 ":" 冒号开头
格式:: 数字 \r\n
eg: ":1000\r\n"- 4.大字符串类型 Bulk Strings, 以 " 字符串的长度 \r\n 字符串 \r\n
字符串不能包含 CR或者 LF(不允许换行);
eg: "$$6\r\nfoobar\r\n" 其中字符串为 foobar,而6就是foobar的字符长度
"$0\r\n\r\n" 空字符串
"$-1\r\n" null- 5.数组类型 Arrays,以 ""星号开头
格式: 数组元素个数 \r\n 其他所有类型 (结尾不需要\r\n)
注意:只有元素个数后面的\r\n是属于该数组的,结尾的\r\n一般是元素的
https://www.cnblogs.com/51QA/p/8709959.html
Redis配置中(redis.windows.conf)可配置持久化策略。配置开关默认打开,若关闭(非持久化),数据只存储缓存中,若打开,数据将按配置策略落地到磁盘。重启服务后,缓存中的数据将丢失。
save 900 1
save 300 10
save 60 10000
//分别表示900s(15分钟)内有1个更改,300s(5分钟)内有10个更改以及60s内有10000个更改
42.1 如何规避redis数据库内存溢出风险?
1.消耗内存的模块需尽快改造或停止,业务中使用redis需设置有效时间。
2.现网针对基础软件服务的监控(磁盘、内存等)需完善。
3.开发设计评审时关注开发模型对redis的写频率,以及消耗大小,评估对redis内存消耗的风险。
42.2 内存溢出和内存泄漏的区别
https://blog.csdn.net/buutterfly/article/details/6617375
- 内存溢出 out of memory,
是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。- 内存泄露 memory leak,
是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。- memory leak会最终会导致out of memory!
- 内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
- 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.
RabbitMQ能承载多少高并发于服务器性能有关,测试4核8G的服务器上RabbitMQ的并发能破1000
https://www.cnblogs.com/jasontec/p/9699242.html
单机版
特点:简单
问题:
1、内存容量有限 2、处理能力有限 3、无法高可用。
主从复制
Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。 只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步 给从服务器,从而一直保证主从服务器的数据相同。
特点:
1、master/slave 角色
2、master/slave 数据相同
3、降低 master 读压力在转交从库
问题:
无法保证高可用
没有解决 master 写的压力
哨兵
Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移。其中三个特性:
监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。
特点:
1、保证高可用
2、监控各个节点
3、自动故障迁移
缺点:主从模式,切换需要时间丢数据
没有解决 master 写的压力
集群(proxy 型):
Twemproxy 是一个 Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器; Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和 redis 协议。
特点:
1、多种 hash 算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
2、支持失败节点自动删除
3、后端 Sharding 分片逻辑对业务透明,业务方的读写方式和操作单个 Redis 一致
缺点:
增加了新的 proxy,需要维护其高可用。
集群(直连型):
从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
特点:
1、无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
2、数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
3、可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
4、高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本
5、实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。
缺点:
1、资源隔离性较差,容易出现相互影响的情况。
2、数据通过异步复制,不保证数据的强一致性
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
45.1 如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
缺点:
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。
能不能生产一次消费多次呢?
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
如何避免?
1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
2:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免?
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
3:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
https://blog.csdn.net/qq_22855325/article/details/76087138
1、一条SQL语句插入多条数据
合并后日志量(MySQL的binlog和innodb的事务让日志)减少了,降低日志刷盘的数据量和频率,从而提高效率。通过合并SQL语句,同时也能减少SQL语句解析的次数,减少网络传输的IO。
2、在事务中进行插入处理。
使用事务可以提高数据的插入效率,这是因为进行一个INSERT操作时,MySQL内部会建立一个事务,在事务内才进行真正插入处理操作。通过使用事务可以减少创建事务的消耗,所有插入都在执行后才进行提交操作。
3、数据有序插入。
由于数据库插入时,需要维护索引数据,无序的记录会增大维护索引的成本。我们可以参照InnoDB使用的B+tree索引,如果每次插入记录都在索引的最后面,索引的定位效率很高,并且对索引调整较小;如果插入的记录在索引中间,需要B+tree进行分裂合并等处理,会消耗比较多计算资源,并且插入记录的索引定位效率会下降,数据量较大时会有频繁的磁盘操作。
4、性能综合测试这里。
提供了同时使用上面三种方法进行INSERT效率优化的测试
合并数据+事务的方法在较小数据量时,性能提高是很明显的,数据量较大时(1千万以上),性能会急剧下降,这是由于此时数据量超过了innodb_buffer的容量,每次定位索引涉及较多的磁盘读写操作,性能下降较快。而使用合并数据+事务+有序数据的方式在数据量达到千万级以上表现依旧是良好,在数据量较大时,有序数据索引定位较为方便,不需要频繁对磁盘进行读写操作,所以可以维持较高的性能。
注意事项:
SQL语句是有长度限制,在进行数据合并在同一SQL中务必不能超过SQL长度限制,通过max_allowed_packet配置可以修改,默认是1M,测试时修改为8M。
事务需要控制大小,事务太大可能会影响执行的效率。MySQL有innodb_log_buffer_size配置项,超过这个值会把innodb的数据刷到磁盘中,这时,效率会有所下降。所以比较好的做法是,在数据达到这个这个值前进行事务提交。
https://www.cnblogs.com/Bruce3555/p/6898740.html
1.简介
RabbitMQ中,消息丢失可以简单的分为两种:客户端丢失和服务端丢失。针对这两种消息丢失,RabbitMQ都给出了相应的解决方案。
2.防止客户端丢失消息
默认情况下,RabbitMQ会平均的分发消息给C1C2,假设一个任务的执行时间非常长,在执行过程中,客户端挂了(连接断开),那么,该客户端正在处理且未完成的消息,以及分配给它还没来得及执行的消息,都将丢失。因为默认情况下,RabbitMQ分发完消息后,就会从内存中把消息删除掉。
3. 消息确认(Message acknowledgment)
为了解决上述问题,RabbitMQ引入了消息确认机制,当消息处理完成后,给Server端发送一个确认消息,来告诉服务端可以删除该消息了,如果连接断开的时候,Server端没有收到消费者发出的确认信息,则会把消息转发给其他保持在线的消费者。
deliveryTag简单来说,就是RabbitMQ内部用来区分消息的一个标签,从envelope中获取就行了
忘记确认将引起内存泄漏
4. 消息的持久化
现在,消费者宕机已经无法影响到我们的消息了,但如果RabbitMQ重启了,消息依然会丢失。所幸的是,RabbitMQ提供了持久化的机制,将内存中的消息持久化到硬盘上,即使重启RabbitMQ,消息也不会丢失。但是,仍然有一个非常短暂的时间窗口(RabbitMQ收到消息还没来得及存到硬盘上)会导致消息丢失,如果需要严格的控制,可以参考官方文档
要使用RabbitMQ的消息持久化,在声明队列时设置一个参数即可
bollean durable = true;
注意,RabbitMQ不允许对一个已经存在的队列用不同的参数重新声明,对于试图这么做的程序,会报错,所以,改动之前代码之前,要在控制台中把原来的队列删除
https://blog.csdn.net/srj1095530512/article/details/82529759
Json是一种轻量级的数据交换格式,采用一种“键:值”对的文本格式来存储和表示数据,在系统交换数据过程中常常被使用,是一种理想的数据交换语言。
1.1:JSON对象
{
"ID": 1001,
"name": "张三",
"age": 24
}
第一个数据就是一个Json对象,观察它的数据形式,可以得出以下语法:
1:数据在花括号中
2:数据以"键:值"对的形式出现(其中键多以字符串形式出现,值可取字符串,数值,甚至其他json对象)
3:每两个"键:值"对以逗号分隔(最后一个"键:值"对省略逗号)
遵守上面3点,便可以形成一个json对象。
1.2:JSON对象数组
[
{"ID": 1001, "name": "张三", "age": 24},
{"ID": 1002, "name": "李四", "age": 25},
{"ID": 1003, "name": "王五", "age": 22}
]
1:数据在方括号中(可理解为数组)
2:方括号中每个数据以json对象形式出现
3:每两个数据以逗号分隔(最后一个无需逗号)
遵守上面3点,便可形成一个json对象数组(及一个数组中,存储了多个json对象)
理解了上面两种基本的形式,我们就可以得出其他的数据形式,例如下面这个:
{
"部门名称":"研发部",
"部门成员":[
{"ID": 1001, "name": "张三", "age": 24},
{"ID": 1002, "name": "李四", "age": 25},
{"ID": 1003, "name": "王五", "age": 22}],
"部门位置":"xx楼21号"
}
1.3:JSON字符串
JSON字符串也是在平时开发中使用较多的,json字符串应满足以下条件:
1:它必须是一个字符串,由" "或者' '包裹数据,支持字符串的各种操作
2:里面的数据格式应该要满足其中一个格式,可以是json对象,也可以是json对象数组或者是两种基本形式的组合变形。
总结:json可以简单的分为基本形式:json对象,json对象数组。两种基本格式组合变形出其他的形式,但其本质还是json对象或者json对象数组中的一种。json对象或对象数组可以转化为json字符串,使用于不同的场合。
2.1:fastjson简介与jar下载
fastjson.jar是阿里巴巴开发的一款专门用于Java开发的包,可以方便的实现json对象与JavaBean对象的转换,实现JavaBean对象与json字符串的转换,实现json对象与json字符串的转换。除了这个fastjson以外,还有Google开发的Gson包,其他形式的如net.sf.json包,都可以实现json的转换。方法名称不同而已,最后的实现结果都是一样的。
1. toJSONString 对象或集合(内套集合也可以)转JSON对象
JSONObject json = new JSONObject();
json.toJSONString(user)
2. JSON格式字符串与JSON对象之间的转换。
String j = "{\"goodslist\":[{\"goods_id\":1}],\"name\":\"张三\"}";
JSONObject jsonObject = JSONObject.parseObject(j);
或者
JSONObject jsonObject = JSON.parseObject(j);
3. json字符串-数组类型与JSONArray之间的转换
//json字符串-数组类型
private static final String JSON_ARRAY_STR = "[{\"studentName\":\"lily\",\"studentAge\":12},{\"studentName\":\"lucy\",\"studentAge\":15}]";
JSONArray jsonArray = JSON.parseArray(JSON_ARRAY_STR);
4. 复杂json格式字符串与JSONObject之间的转换
JSONObject jsonObject = JSON.parseObject(j);
5. json字符串转JAVA对象
User user = JSON.parseObject(j,new TypeReference<User>() {} );
6. json字符串集合 转LIST对象
String str = "[{\"goods_id\":1},{\"goods_id\":2}]";
List<Goods> goodslist = JSON.parseObject(str,new TypeReference<ArrayList<Goods>>(){});
application.properties 文件和 application.yml 文件有什么区别呢?
yml文件的好处,天然的树状结构,一目了然,实质上跟properties是差不多的。
官方给的很多demo,都是用yml文件配置的。
注意点:
1,原有的key,例如spring.jpa.properties.hibernate.dialect,按“.”分割,都变成树状的配置
2,key后面的冒号,后面一定要跟一个空格
3,把原有的application.properties删掉。然后一定要执行一下 maven -X clean install
正常的情况是先加载yml,接下来加载properties文件。如果相同的配置存在于两个文件中。最后会使用properties中的配置。最后读取的优先级最高。
两个配置文件中的端口号不一样会读取properties中的端口号。
YAML:以数据为中心,比json、xml等更适合做配置文件;
YAML:配置例子
server:
port: 8081
相当于xml的
<server>
<port>8081</port>
</server>
1、基本语法
k:(空格)v:表示一对键值对(空格必须有);
以空格的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的
server:
port: 8081
path: /hello
属性和值也是大小写敏感;
2、值的写法
2.1字面量:普通的值(数字,字符串,布尔)
k: v:字面直接来写;
字符串默认不用加上单引号或者双引号;
“”:双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思
name: “zhangsan \n lisi”:输出;zhangsan 换行 lisi
‘’:单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据
name: ‘zhangsan \n lisi’:输出;zhangsan \n lisi
2.2 对象、Map(属性和值)(键值对):
k: v:在下一行来写对象的属性和值的关系;注意缩进
对象还是k: v的方式
friends:
lastName: zhangsan
age: 2
行内写法:
friends: {lastName: zhangsan,age: 18}
2.3 数组(List、Set):
用- 值表示数组中的一个元素
pets:
- cat
- dog
- pig
行内写法
pets: [cat,dog,pig]
spring和springMvc:
- spring是一个一站式的轻量级的java开发框架,核心是控制反转(IOC)和面向切面(AOP),针对于开发的WEB层(springMvc)、业务层(Ioc)、持久层(jdbcTemplate)等都提供了多种配置解决方案;
- springMvc是spring基础之上的一个MVC框架,主要处理web开发的路径映射和视图渲染,属于spring框架中WEB层开发的一部分;
springMvc和springBoot:
- springMvc属于一个企业WEB开发的MVC框架,涵盖面包括前端视图开发、文件配置、后台接口逻辑开发等,XML、config等配置相对比较繁琐复杂;
- springBoot框架相对于springMvc框架来说,更专注于开发微服务后台接口,不开发前端视图,同时遵循默认优于配置,简化了插件配置流程,不需要配置xml,相对springmvc,大大简化了配置流程
springBoot和springCloud:
- spring boot使用了默认大于配置的理念,集成了快速开发的spring多个插件,同时自动过滤不需要配置的多余的插件,简化了项目的开发配置流程,一定程度上取消xml配置,是一套快速配置开发的脚手架,能快速开发单个微服务;
- spring cloud大部分的功能插件都是基于springBoot去实现的,springCloud关注于全局的微服务整合和管理,将多个springBoot单体微服务进行整合以及管理; springCloud依赖于springBoot开发,而springBoot可以独立开发;
总结:
1. Spring 框架就像一个家族,有众多衍生产品例如 boot、security、jpa等等。但他们的基础都是Spring的ioc、aop等. ioc 提供了依赖注入的容器, aop解决了面向横切面编程,然后在此两者的基础上实现了其他延伸产品的高级功能;
2. springMvc是基于Servlet 的一个MVC框架主要解决WEB开发的问题,因为Spring的配置非常复杂,各种XML、JavaConfig、servlet处理起来比较繁琐;
3. 为了简化开发者的使用,从而创造性地推出了springBoot框架,默认优于配置,简化了springMvc的配置流程;
但区别于springMvc的是,springBoot专注于微服务方面的接口开发,和前端解耦,虽然springBoot也可以做成springMvc前后台一起开发,但是这就有点不符合springBoot框架的初衷了;
4. 对于springCloud框架来说,它和springBoot一样,注重的是微服务的开发,但是springCloud更关注的是全局微服务的整合和管理,相当于管理多个springBoot框架的单体微服务
NullPointerException 空指针异常
ArrayIndexOutOfBoundsException 索引越界异常
InputFormatException 输入类型不匹配
SQLException SQL异常
IllegalArgumentException 非法参数
NumberFormatException 类型转换异常
Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出「Error类和Exception类」。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
处理方法:
1.「try()catch(){}」
try{
// 程序代码
}catch(ExceptionName e1){
//Catch 块
}
2.「throw」
throw 关键字作用是抛出一个异常,抛出的时候是抛出的是一个异常类的实例化对象,在异常处理中,try 语句要捕获的是一个异常对象,那么此异常对象也可以自己抛出
3.「throws」
定义一个方法的时候可以使用 throws 关键字声明。使用 throws 关键字声明的方法表示此方法不处理异常,而交给方法调用处进行处理。
Integer a = 1000,Integer b = 1000,a==b 结果为「false」
Integer a = 1,Integer b = 1,a==b 结果为「true」
这道题主要考察 Integer 包装类缓存的范围,「在-128~127之间会缓存起来」,比较的是直接缓存的数据,在此之外比较的是对象
浅拷贝并不是真的拷贝,只是「复制指向某个对象的指针」,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝会另外「创造一个一模一样的对象」,新对象跟原对象不共享内存,修改新对象不会改到原对象。
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
示例代码:
// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer. append(“abcdefg”);
System. out. println(stringBuffer. reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder. append(“abcdefg”);
System. out. println(stringBuilder. reverse()); // gfedcba
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。