日常学习工作中的JAVA指南
JAVA异常
我们一般对运行期间出现的异常进行处理RuntimeException、编译期出现的异常可以直接在编译时解决。
图解:
JAVA异常的实践原则:
- 异常如果返回的是返回码,一定要标注返回码对应的异常
- 保持代码整洁,别把一大段代码包含在try中,或者try中包含try
- 自定义一些通用异常类
- 捕获的异常要具体,不要使用Exception全包含
JAVA编码中常出现的异常:
- 并发修改异常ConcurrentModificationException
//不会报错
ArrayList<Integer> objects = new ArrayList<>();
objects.add(1);
objects.add(2);
for (Integer object : objects) {
objects.remove(object);
}
//报错
ArrayList<Integer> objects2 = new ArrayList<>(Arrays.asList(1,2,3));
for (Integer object : objects2) {
objects2.remove(object);
}
//解决方案:可以使用addAll方法或者Iterator迭代器
- 类型转换异常ClassCastException
Work extends User
Manager extends User
使用多态new对象
User user1 = new Work();
User user2 = new Manager();
这样强制类型转换可以
Work w = (Work) user1;
这样就会出现转换异常
Work w = (Work) user2;
//解决方案:如果真的知道类型可以 (类名)强转
如果不知道可以输出真实的类名。instanceof 确认是否是需要的类型,然后再强转
if (e1 instanceof SysUser) {
SysUser a2 = (SysUser) e1;
}
对于IO,最好使用try-with-resources操作。防止因忘记释放资源而造成的性能下降。
//单个资源
try (InputStream inputStream = new FileInputStream("")){}
//多个资源
try (InputStream inputStream = new FileInputStream("");
OutputStream outputStream = new FileOutputStream("");){
//...读写操作
}
空指针NPE
出现空指针情况
空指针是运行时异常java.lang.NullPointerException
public class UnboxNpe {
static int add(int a, int b){
return a + b;
}
public static void main(String[] args) {
//变量自动拆箱时出现空指针
Integer a = null;
int b = a;
//变量自动装箱时出现的空指针
Integer c = null;
Integer d = null;
add(c, d);
}
}
javac命令用于将 java 源文件编译为class字节码文件。(如 javac UnboxNpe.class)
java命令可以运行class字节码文件(如java UnboxNpe)
如何避免空指针
- 返回值为具体的值不为null
- 尽量做好初始化操作
- 不知道值是否为null时一定要做好判断
合理使用Optional规避空指针
尽量不使用get和isPresent这种无意义的方法
多使用orelse map orelseget map 等有意义的方法
public static void main(String[] args) {
SysUser sysUser = null;
Optional<SysUser> sysUser = Optional.ofNullable(sysUser);
sysUser.orElse(new SysUser());
sysUser.orElseGet(() -> new SysUser());
sysUser.orElseThrow(RuntimeException::new);
//以上操作,为null会直接操作,map可以无线级联
sysUser.map(SysUser::getDept).map(SysDept::getAreaId).orElse(0L);
}
JAVA计算、日期常见错误
bigDecimal数值计算
BigDecimal bigDecimal = new BigDecimal("12.222");
//BigDecimal bigDecimal1 = bigDecimal.setScale(2);//精度不够、报错
BigDecimal bigDecimal2 = bigDecimal.setScale(12);//12.22200000000
//BigDecimal 进行四舍五入
BigDecimal bigDecimal3 = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP);
//BigDecimal做除法时除不尽
bigDecimal.divide(new BigDecimal(3));//除不尽报错
//使用四舍五入不用那么多精度
System.out.println(bigDecimal.divide(new BigDecimal(3)).setScale(0,BigDecimal.ROUND_HALF_UP));//4
SimpleDateFormat会出现线程安全问题
解决方案:
- 定义为局部变量不共享(常用)
- 使用ThreadLocal(需要进行维护2)
- 使用synchronized锁(性能要求高不常用)
- 使用Java8的DateTimeFormatter是线程安全的
使用ThreadLocal
static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args) throws IOException{
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100,
1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1000));
String d1 = "2020-01-01 00:00:00";
while (true){
threadPoolExecutor.execute(() -> {
Date parse = null;
try {
parse = threadLocal.get().parse(d1);
} catch (ParseException e) {
e.printStackTrace();
}
String format = threadLocal.get().format(parse);
System.out.println(format);
});
}
使用synchronized锁
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100,
1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1000));
String d1 = "2020-01-01 00:00:00";
while (true){
threadPoolExecutor.execute(() -> {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:dd");
synchronized (simpleDateFormat) {
Date parse = null;
try {
parse = simpleDateFormat.parse(d1);
} catch (ParseException e) {
e.printStackTrace();
}
String format = simpleDateFormat.format(parse);
System.out.println(format);
}
});
}
equals和hashCode方法重写的必要性
业务场景:如果对象中名字字段相同,就表示这两个对象相等(重写类的equals方法即可)
User user1 = new User("xy",1);
User user2 = new User("xy",1);
User user3 = new User("xy",3);
//在类未重写情况下比较
System.out.println(user1.equals(user2));//true
System.out.println(user1.equals(user3));//false
//重写类的equals方法
System.out.println(user1.equals(user2));//true
System.out.println(user1.equals(user3));//true
@Override
public boolean equals(Object o) {
//默认的equals
/* if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) &&
Objects.equals(age, user.age);*/
//重写 名字相等则相等
return this.name.equals(((User) o).name);
}
业务场景:HashMap、HashSet存放名字字段相同的类两个对象时去重存储(重写equals方法还不够,要重写hashcode方法)
原因:HashMap与HashSet存储是依靠键的hash值来计算出在哈希数组中的下标位置。
Set<User> set = new HashSet<User>();//HashMap同理
set.add(user1);
set.add(user2);
System.out.println(set.size());//不重写hashcode方法 2
System.out.println(set.size());//重写hashcode方法 1
@Override
public int hashCode() {
return Objects.hash(name, age);
}
JDK1.8下的接口、函数式接口、lambda表达式、Stream流操作
接口静态方法与默认方法
接口中使用默认方法和静态方法:
- 静态方法:可以把常用的工具类写在接口里,直接调用。如Comparator中的comparing静态方法
- 默认方法:经常用在接口中写配置类。实现接口的类可以选择重写此方法,调用时优先调用重写的default方法
//静态方法
public interface IBaseWorking {
void baseCoding();
void baseTesting();
//写一个此接口的专用工具 实现此接口的类不用重写此方法
static void getTime(){
System.out.println(LocalDate.now());
}
}
//例如jdk1.8中的Comparator接口就有静态方法可以直接调用
ArrayList<Integer> list = new ArrayList(Arrays.asList(1,2,3));
Integer integer = list.stream().max(Comparator.comparing(value -> value)).get();
Integer integer1 = list.stream().min(Comparator.comparing(value -> value)).get();
System.out.println(integer);
System.out.println(integer1);
IBaseWorking.getTime();
//默认方法
public class Device implements IBaseWorking{
String iPhone;
}
public interface IBaseWorking {
default void baseCoding(){
System.out.println(LocalDate.now());
};
default void baseTesting(){};
static void getTime(){
System.out.println(LocalDate.now());
}
}
new Device().baseCoding();
//但是默认方法能重写会导致多继承问题
public class Device implements IBaseWorking1, IBaseWorking2{
String iPhone;
}
两个接口中都有default void baseCoding(){};相同签名接口的方法。子类可以覆盖并重写来解决
函数式编程的四种常用方法
我的另一篇文章有写到 包括自定义函数式接口
stream流
中间操作又可以分为无状态(Stateless)与有状态(Stateful)操作
- 前者是指元素的处理不受之前元素的影响,
- 后者是指该操作只有拿到所有元素之后才能继续下去。
终结操作又可以分为短路(Short-circuiting)与非短路(Unshort-circuiting)操作
- 前者是指遇到某些符合条件的元素就可以得到最终结果,
- 后者是指必须处理完所有元素才能得到最终结果。
不常用的几个操作解析
//reduce 解析参考 https://www.cnblogs.com/MrYuChen-Blog/p/14061320.html
List<String> list = new ArrayList<String>(3){{add("1");add("3");add("2");}};
//reduce1 累加
int asInt1 = list.stream().mapToInt(Integer::parseInt).skip(1).peek(System.out::println).reduce((x, y) -> x += y).getAsInt();
System.out.println(asInt1);
//reduce2 接受初始值累加
int asInt2 = list.stream().mapToInt(Integer::parseInt).skip(1).peek(System.out::println).reduce(1000, (x, y) -> x += y);
System.out.println(asInt2);
//reduce3 数据的操作与添加数组
ArrayList<Integer> newList = new ArrayList<>();
ArrayList<Integer> accResult_s = Stream.of(1,2,3,4)
.reduce(newList,
(acc, item) -> {
acc.add(item);
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("BiFunction");
return acc;
}, (acc, item) -> null);
System.out.println("accResult_s: " + accResult_s);
List<Integer> IntegerList = new ArrayList<Integer>(3){{add(1);add(3);add(2);}};
//peek操作一般用于debug
List<Integer> skip = IntegerList.stream().skip(1).peek(System.out::println).collect(Collectors.toList());
System.out.println(skip.toString());
List<Integer> limit = IntegerList.stream().limit(1).peek(System.out::println).collect(Collectors.toList());
System.out.println(limit.toString());
Java序列化、泛型、反射、深浅拷贝
序列化
序列化:将对象写入IO流中
反序列化:从IO流恢复成对象
Serializable是个给jvm标记的接口不用实现任何方法,表示此类进行序列化和反序列化
public static void main(String[] args) throws Exception{
//序列化的步骤
//指定序列化存储位置
File file = new File("D:\\sp_course\\xuliehua.txt");
//创建输出流
//输出可序列化对象
//关闭输出流
try(ObjectOutputStream fileOutputStream = new ObjectOutputStream(new FileOutputStream(file))){
People people = new People();
people.setName("1");
fileOutputStream.writeObject(people);
}
//反序列化步骤
try(ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file))){
People object = (People)objectInputStream.readObject();
System.out.println(object.name);
}
}
实现了Serializable接口却报错原因?
- 子类实现序列化接口,父类没有实现序列化但是有无参构造函数,子类就可以序列化
- 类实现了序列化,类中的引用对象没实现序列化,此类进行序列化操作会报错
- 在对象第一次序列化以后,无论后面如何改变这个对象,反序列化以后都是第一次保存的值(因为序列化时会去硬盘或者内存中查看是否有这个序列化码,有的话就不会覆盖了)
泛型初识
- 什么是泛型:泛型就是将固定的类型转换为动态的参数化类型
- 为什么要使用泛型:能够在不创建新类型的情况下,通过泛型指定的不同类型来控制形参具体的限制类型
- 泛型有泛型类、泛型接口、泛型方法三种
public class GenericClass<T> {
T name;
/**
* 这不是一个泛型方法
* */
public GenericClass(T t){
this.name = t;
}
/**
* 这是一个泛型方法,但这里的 T 可以与类上申明的 T 不是一个类型
* */
public <T> void GenericMethod(T newT){
System.out.println(newT);
}
}
public interface GenericInterface<T> {
/**
* 泛型接口与泛型类的使用大致相同
* */
T getT(T t);
}
public class GenericMain {
public static <T> T GenericMethod(T t){
return t;
}
public static void main(String[] args) {
GenericClass<People> peopleGenericClass = new GenericClass<People>();
peopleGenericClass.GenericMethod(new Device());
Device device = GenericMethod(new Device());
List<Integer> list = new ArrayList<>();
list.add(1);
list.getClass().getMethod("add", Object.class).invoke(list,"111");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i)); //因为listgeti返回的是个泛型所以可以这样玩,如果是用foreach就不行了
}
}
public void showKeyValue2(GenericClass<?> obj){
//?是实参可以表示Object
}
}
泛型认识
- 泛型是先检查再编译
- 泛型不支持继承
- 泛型变量不能是基本数据类型
以public interface List<E> extends Collection<E> {}为例
//1. 泛型是先检查再编译
List<Integer> list = new ArrayList<>();
list.add(1);
list.add("1");//再idea检查时就会报错
//2. 泛型不支持继承
List<Object> list2 = new ArrayList<>();
List<Integer> list1 = list2;//会报错
List<Integer> list2 = new ArrayList<>();
List<Object> list1 = list2;//也会报错(但是string可以强转成object所以不会报类型转换异常)
//3. 泛型变量不能是基本数据类型
List<int>//错误
初级优化:用List替代原始类型
高级优化:明确类型List
反射
什么是反射:允许运行中的java程序获取自身的信息,并可以操作类或者对象的内部属性
反射的应用场景:
- 开发通用框架
- 动态代理
- 注解
- 可扩展性功能
反射同样存在着缺陷:
- 性能开销
- 破坏封装
- 内部曝光
反射方法使用:
public class Worker extends People {
private String getName(String name){
return Worker.class.getName() + "name";
}
private String getAge(int age){
return Worker.class.getName() + age;
}
}
public static void main(String[] args) throws Exception{
Class<Worker> workerClass = Worker.class;
Method[] declaredMethods = workerClass.getDeclaredMethods();//获得所有方法
//获得指定方法名
Method getName = workerClass.getDeclaredMethod("getName", String.class);
getName.setAccessible(true);//设置可操作
//执行方法
String invoke = (String) getName.invoke(workerClass.newInstance(), "19");
System.out.println(invoke);
Method getAge = workerClass.getDeclaredMethod("getAge", int.class);//因为方法定义的是基本类型,如果这里写Integer会报nosuchmethod错
}
深浅拷贝
拷贝:一个对象中的属性拷贝到另外一个对象中
深浅拷贝的区别在于是否使用相同的内存地址
Cloneable标志对象是否可以被拷贝的接口,如果类没实现接口直接调用Object的clone方法会报错,Object的clone方法是浅拷贝
//浅拷贝1
Teacher t = new Teacher();
Teacher t1 = t;
//浅拷贝2 必须标志Cloneable接口,和重写clone方法
@Data
@NoArgsConstructor
@AllArgsConstructor
public class People implements Cloneable{
String name;
int age;
Rabbit rabbit;
protected People clone() throws CloneNotSupportedException{
return (People) super.clone();
}
}
public static void main(String[] args) throws Exception{
People people = new People("牛牛",1, new Rabbit("员工A",1));
People clone = people.clone();
people.setName("羊羊");
people.getRabbit().setName("员工B");
System.out.println(people.toString());//People(name=羊羊, age=1, rabbit=Rabbit(name=员工B, age=1))
System.out.println(clone.toString());//People(name=牛牛, age=1, rabbit=Rabbit(name=员工B, age=1))
//因为String是特殊的引用数据类型每次都会new,所以没有影响。但标准的引用类就显示出了是浅拷贝
}
如何实现深拷贝
- 引用类型也重写clone、然后重写主clone类的方法
- 序列化输入输出流
//1. 引用类型也重写clone、然后重写主clone类的方法
@AllArgsConstructor
@Data
public class Rabbit implements Cloneable{
String name;
int age;
protected Rabbit clone() throws CloneNotSupportedException{
return (Rabbit) super.clone();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class People implements Cloneable{
String name;
int age;
Rabbit rabbit;
protected People clone() throws CloneNotSupportedException{
People people = (People) super.clone();
people.rabbit = people.getRabbit().clone();
return people;
}
}
public class Main {
public static void main(String[] args) throws Exception{
People people = new People("牛牛",1, new Rabbit("员工A",1));
People clone = people.clone();
people.setName("羊羊");
people.getRabbit().setName("员工B");
System.out.println(people.toString());//People(name=羊羊, age=1, rabbit=Rabbit(name=员工B, age=1))
System.out.println(clone.toString());//People(name=牛牛, age=1, rabbit=Rabbit(name=员工A, age=1))
}
}
//序列化输入输出流
@Data
@NoArgsConstructor
@AllArgsConstructor
public class People implements Serializable{
String name;
int age;
Rabbit rabbit;
@SneakyThrows
protected People clone(){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (People) ois.readObject();
}
}
@AllArgsConstructor
@Data
public class Rabbit implements Serializable {
String name;
int age;
protected Rabbit clone() throws CloneNotSupportedException{
return (Rabbit) super.clone();
}
}
public class Main {
public static void main(String[] args) throws Exception{
People people = new People("牛牛",1, new Rabbit("员工A",1));
People clone = people.clone();
people.setName("羊羊");
people.getRabbit().setName("员工B");
System.out.println(people.toString());//People(name=羊羊, age=1, rabbit=Rabbit(name=员工B, age=1))
System.out.println(clone.toString());//People(name=牛牛, age=1, rabbit=Rabbit(name=员工A, age=1))
}
}
线程安全
线程不安全操作变量例子
public class SynchronizedActive implements Runnable{
private int a = 0;
@Override
public void run() {
while (true){
if (a < 10000){
System.out.println("start a = " + a);
a++;
System.out.println("end a = " + a);
}else {
break;
}
}
}
}
public class Main {
public static void main(String[] args) {
SynchronizedActive synchronizedActive = new SynchronizedActive();
Thread thread1 = new Thread(synchronizedActive);
Thread thread2 = new Thread(synchronizedActive);
Thread thread3 = new Thread(synchronizedActive);
Thread thread4 = new Thread(synchronizedActive);
Thread thread5 = new Thread(synchronizedActive);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
//会打印出end a=10001
//解决办法是:只要将run方法加以synchronized修饰即可
保证线程安全方法
- synchronized同步锁
- JAVA提供了java.util.concurrent.atomic原子操作(乐观锁)
synchronized修饰的方法不会被继承,子类需重新声明
synchronized可修饰对象、方法、静态方法变量(也就是对类进行锁)
public synchronized int fun1() { // 锁住 this 从这里
// ...
} //到这里
以下写法和上面效果一样,因为锁了整个object (就是class object)
public int func1() {
synchronized (this) { // 锁住 this 从这里
// ...
} // 到这里
}
标在代码块中最主要的特征是,锁的范围缩小了。比如你只想锁住某一个object
Object obj1 = new Object();
public void fun1() {
synchronized(obj1) { // 锁住 obj1从这里
// ...
} // 到这里
}
cas乐观锁会出现ABA问题(业主要看业务场景会不会出现此类问题。B的操作被其他操作类的线程所忽略),JDK中提供了AtomicMarkableReference、AtomicStampedReference来解决ABA问题。点击查看参考博客
修改上面方法
private AtomicInteger a = new AtomicInteger(0);
@Override
public void run() {
while (true){
if (a.incrementAndGet() < 10001){
System.out.println("start a = " + a.get());
}else {
break;
}
}
}
BlockingQueue阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
阻塞队列提供了四种处理方法:
多线程对ArrayList进行操作出现线程安全问题
可以使用CopyOnWriteArrayList、CopyOnWriteArraySet进行并发读写
CopyOnWriteArrayList问题:
- 内存占用问题
- 一致性问题(如果你希望写进去的数据马上能读到就不行,只能保证最终一致性,不能保证实时一致性)
适用场景适合读多写少的并发场景,如黑名单白名单。减少扩容开销,使用批量添加
Collections.synchronizedList() 与 CopyOnWriteArrayList 区别:
- 都可以进行线程安全的读写操作
- synchronizedList对读进行了synchronized锁操作所以读性能低
- CopyOnWriteArrayList对写进行了锁,并且对数组进行大量copy操作,所以写性能差
- 根据不同的业务场景,选用不同的线程安全集合,写多读少Collections.synchronizedList(),读多写少CopyOnWriteArrayList
线程池问题
JDK线程池核心类:ThreadPoolExcutor
线程池运行状态:
- running
- shutdown 线程池执行完后销毁
- shutdownnow 线程池马上销毁
- tidying
- terminated
JDK线程池场景问题:
使用jdk的固定大小的线程池可能会出现内存急剧占用上升的问题:因为未处理的数据一直在内存中排队
@AllArgsConstructor
public class Reading implements Runnable {
private int count;
private String name;
public void run(){
while (count > 0){
System.out.println(Thread.currentThread() + ":::" + name + "::::" + count );
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
--count;
}
}
}
public static void main(String[] args) throws Exception{
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
Reading reading1 = new Reading(3,"java-1");
Reading reading2 = new Reading(3,"java-2");
Reading reading3 = new Reading(3,"java-3");
Reading reading4 = new Reading(3,"java-4");
Reading reading5 = new Reading(3,"java-5");
fixedThreadPool.execute(reading1);
fixedThreadPool.execute(reading2);
fixedThreadPool.execute(reading3);
fixedThreadPool.execute(reading4);
fixedThreadPool.execute(reading5);
fixedThreadPool.shutdown();
//未被消费的数据会在阻塞队列里一直等待,会造成内存庞大,导致一直gc或者内存溢出
}
自定义线程池要正确使用参数
常用自定义参数
int corePoolSize 当有任务执行时,创建核心线程
int maximumPoolSize 当核心和阻塞都满时 才来判断是否已到最大线程数
long keepAliveTime 空闲线程最大空闲时间回收
TimeUnit unit,
BlockingQueue<Runnable> workQueue (无界/有界的阻塞队列)
RejectedExecutionHandler handler 当已达到最大线程数时的拒绝策略
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
拒绝策略有(默认拒绝策略是马上抛出异常(AbortPolicy )):
- AbortPolicy ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- CallerRunsPolicy ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
- DiscardPolicy ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
- DiscardOldestPolicy ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
自定义线程池
当线程池的核心线程数阻塞队列都满时,超过最大线程数,默认拒绝策略是报错
public static void main(String[] args) throws Exception{
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(2, 2, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
for (int i = 0; i < 5; i++) {
threadPoolExecutor.execute(new Reading(3,"java-1"));
}
threadPoolExecutor.shutdown();
}
可以自定义拒绝策略,当出现那种情况时,阻塞队列进行阻塞,不报错
public class Main {
public static void main(String[] args) throws Exception{
ThreadPoolExecutor threadPoolExecutor2 =
new ThreadPoolExecutor(2, 2, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2)
, new DefaultRejectMethod());
for (int i = 0; i < 5; i++) {
threadPoolExecutor2.execute(new Reading(3,"java-1"));
}
threadPoolExecutor2.shutdown();
}
public static class DefaultRejectMethod implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
executor.getQueue().put(r);//阻塞队列的阻塞方法put
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
ThreadLocal使用
- 同一个ThreadLocal所包含的对象,在不同的Thread中有不同副本
- ThreadLocal并不支持继承
- 其实threadlocal维护的是一个弱引用hashmap,如果不初始化就get会报空指针异常
public static void main(String[] args) throws Exception{
ThreadLocal<Rabbit> threadLocal = ThreadLocal.withInitial(() -> new Rabbit("兔兔",1));
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(2, 2, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
threadPoolExecutor.execute(() ->{
print(threadLocal);
});
threadPoolExecutor.execute(() ->{
print(threadLocal);
});
threadPoolExecutor.execute(() ->{
print(threadLocal);
});
threadPoolExecutor.shutdown();
// Rabbit(name=8a1b9aa4-166e-4531-9be2-ce8715cc7b8b, age=1)
// Rabbit(name=7ded6818-292d-4283-a1c3-e14be16668a0, age=1)
// Mon Mar 29 21:58:11 GMT+08:00 2021
// Mon Mar 29 21:58:11 GMT+08:00 2021
// Rabbit(name=192bfda5-3f4e-4651-b532-6c78ccf8a288, age=1)
// Mon Mar 29 21:58:11 GMT+08:00 2021
}
//会打印不同的副本内容
public static void print(ThreadLocal<Rabbit> threadLocal){
threadLocal.get().setName(UUID.randomUUID().toString());
System.out.println(threadLocal.get());
System.out.println(new Date());
}
//ThreadLocal未初始化,则子线程拿不到主线程中定义的对象
public static void main(String[] args) throws Exception{
ThreadLocal<Rabbit> threadLocal = new ThreadLocal<>();
threadLocal.set(new Rabbit("兔兔",1));
Thread thread = new Thread(() -> {
System.out.println("son-thread:" + threadLocal.get());
});
thread.start();
System.out.println("main-thread:" + threadLocal.get());
// main-thread:Rabbit(name=兔兔, age=1)
// son-thread:null
}
//线程池下,如果不使用threadLocal.remove();清理,则打印的是223,现在则是222
public static void main(String[] args) throws Exception{
ThreadPoolExecutor threadPoolExecutor2 =
new ThreadPoolExecutor(2, 2, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2)
, new DefaultRejectMethod());
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
threadPoolExecutor2.execute(() -> {
threadLocal.set(threadLocal.get() + 1);
System.out.println("thread-1::" + threadLocal.get());
threadLocal.remove();
});
threadPoolExecutor2.execute(() -> {
threadLocal.set(threadLocal.get() + 1);
System.out.println("thread-2::" + threadLocal.get());
threadLocal.remove();
});
threadPoolExecutor2.execute(() -> {
threadLocal.set(threadLocal.get() + 1);
System.out.println("thread-3::" + threadLocal.get());
threadLocal.remove();
});
threadPoolExecutor2.shutdown();
}
Spring常用
Bean的命名
如果@Bean、@Service、@component标注的类或方法上时,名字会变成小写交给spring管理
Spring为Bean自动命名后:
Qname = qname
QTname = QTname
QTnameAny = QTnameAny
为了防止spring自动给你的bean命名,也可以自定义名称
@Bean("a")、@Service("a")、@component("a")
@Test
public void test(){
QTnameAay qTname = (QTnameAay)SpringUtils.getBean("QTnameAay");
System.out.println(qTname);
}
@Autowired出现空指针
- 没有将引入类交给spring管理,并且通过new来获取对象
- 把bean定义在了默认扫描包的外面
//1. 没有将引入类交给spring管理,并且通过new来获取对象
public class QTnameAay {
@Autowired
private People people;
public void getName() {
System.out.println(people.getName());
}
}
其他地方 QTnameAay aa = new QTnameAay();aa.getName();会报空指针
//会引入不进去,报空指针。解决办法是把QTnameAay 交给spring管理
//加上@component @Service等注解,然后使用spring工具类获取此QTnameAay类,而不是new。
//(QTnameAay qTname = (QTnameAay)SpringUtils.getBean("QTnameAay");)
//2. 把bean定义在了默认扫描包的外面
解决办法1是在启动类上定义需扫描的包(全局)
@ComponentScan(value = {"com.imooc.spring.inner","com.imooc.spring.outer"})
或者在配置类上添加需要扫描的包(局部),包需有@Component注解交予spring管理
@Configuration
@ComponentScan(value = "com.ruoyi.system.interceptor")
public class ResourcesConfig implements WebMvcConfigurer {
@Autowired
CustomInterceptor customInterceptor;
}
Spring Bean 单例模式
spring的管理的bean都是单例的,在多线程下会造成问题
单例模式优势:
- 从缓存中获取bean快
- 节约新实例的消耗
- 减少垃圾回收
劣势则是bean的线程不安全
@Service
public class ListTest {
List<String> list = null;
@PostConstruct
private void init(){
this.list = new ArrayList<String>();
}
public void add(String str){
this.list.add(str);
}
public List<String> get(){
return this.list;
}
}
@Test
public void test2(){
ListTest listTest1 = (ListTest) ApplicationUtils.getBean("listTest");
ListTest listTest2 = (ListTest) ApplicationUtils.getBean("listTest");
listTest1.add("1");
listTest1.add("2");
System.out.println(listTest1.get()); //[1, 2]
listTest2.add("3");
System.out.println(listTest1.get()); //[1, 2, 3]
//由此可见操作的是同一个bean
}
解决问题:使用原型模式bean
在需要加载的bean上进行scope注解
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
Bean异常
首先我们了解注入bean的注解:
- @Autowired:属于Spring框架,优先默认使用类型( byType )进行注入,找不到在使用名称注入
- @Qualifier:结合@Autowired一起使用,自动注入策略由byType变成 byName
- @Resource:JavaEE自带的注解,默认按byName自动注入,也就是Bean名称自动注入
- @Inject:和@Autowired差不多,但不能与@Qualifier连用
- @Primary:存在多个相同类型的Bean,则@Primary用于定义首选项,@Autowired会优先注入标注了@Primary的配置
常出现的两个bean异常:
- 只定义了接口,但是没有实现,抛出NoSuchBeanDefinitionException异常
- 定义了接口的多个实现类,只使用了@Autowired 实现注入,抛出NoUniqueBeanDefinitionException异常
- 因为自动注入希望注入的类不为null,所以注入的bean没有实现类会报NoSuchBeanDefinitionException
public interface ITemplateService {
void print();
}
@Autowired
ITemplateService iTemplateService;
@Test
public void test4(){
iTemplateService.print();
}
//解决办法:注入时允许实现为null
@Autowired(required = false)
ITemplateService iTemplateService;
- 引入的bean有多个实现,自动注入依照类型注入会找不到,名称注入也找不到,就报NoUniqueBeanDefinitionException
@Service
public class TwoTemplate implements ITemplateService {
@Override
public void print() {
System.out.println("TwoTemplate");
}
}
@Service
public class OneTemplate implements ITemplateService {
@Override
public void print() {
System.out.println("OneTemplate");
}
}
//解决办法:指定名称或者引入名称是某个实现的名称
@Qualifier("OneTemplate")
@Autowired
ITemplateService iTemplateService;
@Autowired
ITemplateService OneTemplate;
为什么要用构造函数引入依赖?
暴露循环依赖问题
出问题例子:
@Service
public class TwoTemplate implements ITemplateService {
private final OneTemplate oneTemplate;
@Autowired
public TwoTemplate(OneTemplate oneTemplate) {
this.oneTemplate = oneTemplate;
}
@Override
public void print() {
System.out.println("TwoTemplate");
}
}
@Service
public class OneTemplate implements ITemplateService {
private final TwoTemplate twoTemplate;
@Autowired
public OneTemplate(TwoTemplate twoTemplate) {
this.twoTemplate = twoTemplate;
}
@Override
public void print() {
System.out.println("OneTemplate");
}
}
@Autowired
ITemplateService OneTemplate;
@Autowired
ITemplateService TwoTemplate;
@Test
public void test4(){
OneTemplate.print();
TwoTemplate.print();
//报错:Requested bean is currently in creation: Is there an unresolvable circular reference?
}
解决办法:
解决办法:
field方式引入
@Autowired
OneTemplate oneTemplate;
setter方式引入
@Autowired
public void setOneTemplate(TwoTemplate twoTemplate) {
this.twoTemplate = twoTemplate;
}
但是经我测试,如果在方法里互相调用的引入方法,那么还是会出现oom的问题+
BeanPostProcessor和BeanFactoryPostProcessor的使用
BeanFactoryPostProcessor的使用
- BeanFactoryPostProcessor是在Spring 容器加载Bean定义XML文件之后,Bean实例化之前执行
- BeanFactoryPostProcessor的执行顺序在BeanPostProcessor之前
- BeanFactoryPostProcessor 与 BeanPostProcessor都是服务于Bean的生命周期中的,只是使用场景和作用略有不同
例如使用场景:若第三方插件是spring的单例模式,但是你想把其中一个类变成原型模式
如果不在类加载之前改变类模式,使用spring默认的单例模式,hashcode就是一样的
Object decode1 = ApplicationUtils.getBean("decodeManage");
Object decode2 = ApplicationUtils.getBean("decodeManage");
System.out.println(decode1.hashCode());
System.out.println(decode2.hashCode());
定义工厂类使用并改变则可以在类初始化之前改变成原型模式
@Component
public class DecodeBeanFactory implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
BeanDefinition decode = factory.getBeanDefinition("decodeManage");
decode.setScope(BeanDefinition.SCOPE_PROTOTYPE);
}
}
使用了@Transactional注解,但是事务并没有生效
注解事务可以定义在类上、接口上、类方法上
类方法上的事务注解可以覆盖类上的事务注解
标注了@Transactional,也未回滚原因:
- trycatch捕获了异常
- 抛出了非RuntimeException异常,即抛出了方法也需要throws出去的异常
- 当前方法没有标注事务注解,调用的方法有标注,此情况下也不会回滚
- @Transactional只能标注在public方法上
- 数据库本身定义的类型就不支持事务,Innodb支持事务
SpringMVC
SpringMVC自定义返回
Controller层实践返回数据
@Controller
@Slf4j
@RequestMapping("/test")
public class smTest {
/**1 通过 springmvc 的 ResponseEntity 自定义返回状态码与内容*/
@GetMapping("/One")
public ResponseEntity<GenerBack<String>> responseEntityOne(){
GenerBack<String> stringGenerBack = new GenerBack<>(0,"");
stringGenerBack.setData("responseEntityTest");
return new ResponseEntity<>(stringGenerBack, HttpStatus.BAD_REQUEST);
}
/**2 @ResponseStatus注解 应用于异常类,返回异常类定义的数据*/
@GetMapping("/Two")
public GenerBack<String> Two(){
throw new CustomException ();
}
//自定义异常ResponseCustomException
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "this is bad request two")
public class ResponseCustomException extends RuntimeException {
}
/**3 @ResponseStatus注解 应用于方法上,直接返回404与reason,前端根据请求返回status做出判断*/
@GetMapping("/Three")
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "this is NOT_FOUND three")
//应用场景,当抛出异常时自定义状态码跳转到404页面,给用户更好的体验
public void Three(){ }
/**4 全局异常 返回http的 自定义状态码*/
@GetMapping("/Four")
public GenerBack<String> Four() throws CustomException{
throw new CustomException("some error");
}
//自定义异常CustomException
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
//使用@ControllerAdvice ( @RestControllerAdvice )和
//@ExceptionHandler注解 捕获Controller返回的自定义异常CustomException
@ControllerAdvice
public class AllException {
@ExceptionHandler(CustomException.class)
public ResponseEntity<GenerBack<String>> responseEntityOne(HttpServletRequest request, Exception ex){
GenerBack<String> stringGenerBack = new GenerBack<>(0,"");
stringGenerBack.setData(ex.getMessage());
return new ResponseEntity<>(stringGenerBack, HttpStatus.BAD_GATEWAY);
}
}
}
@Data
public class GenerBack<T> {
public GenerBack(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
Integer code;
String msg;
T data;
}
时间序列化和反序列化
@DateTimeFormat注解可以解决前后端时间接收与回显问题,但是Post提交body表单还需要其他时间格式转换的方法,如使用@JsonFormat注解,实现自定义格式转换器@JsonDeserialize
一般都使用这两个注解搭配:
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
LocalDateTime birthday;
如果有前端会传入多格式的情况,就要自定义转换器:
@Slf4j
public class DateJacksonConverter extends JsonDeserializer<Date> {
private static final String[] pattern = new String[]{"yyyy-MM-dd HH:mm:ss","yyyy/MM/dd"};
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
Date targetDate = null;
String originDate = jsonParser.getText();//获得传入的时间字符串
if (StringUtils.isNotEmpty(originDate)){
try {
long timeChuo = Long.parseLong(originDate.trim());
targetDate = new Date(timeChuo);//时间戳转换
} catch (Exception e) {
try {
targetDate = DateUtils.parseDate(originDate, DateJacksonConverter.pattern);
System.out.println(targetDate);
} catch (ParseException pe) {
e.printStackTrace();
}
}
}
return targetDate;
}
}
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = DateJacksonConverter.class)
LocalDateTime birthday;
自定义转换器写的太死,如果转换类换了每个地方都要换,所以写spring全局配置类,可以不使用注解:
@Configuration
public class DateConverterConfig {
@Bean
public DateJacksonConverter jacksonConverter(){
return new DateJacksonConverter();
}
@Bean
public Jackson2ObjectMapperFactoryBean jackson2ObjectMapperFactoryBean
(@Autowired DateJacksonConverter dateJacksonConverter){
Jackson2ObjectMapperFactoryBean jackson2ObjectMapperFactoryBean = new Jackson2ObjectMapperFactoryBean();
jackson2ObjectMapperFactoryBean.setDeserializers(dateJacksonConverter);
return jackson2ObjectMapperFactoryBean;
}
}
DateJacksonConverter类追加重写handledType方法
//配置需反序列化的类
@Override
public Class<?> handledType() {
return Date.class;
}
SpringMVC拦截器与Servlet过滤器
拦截器和过滤器里的共享变量要注意线程安全问题,拦截器和过滤器都可定义多个。
Filter过滤器是Web应用程序组件,可以在请求到达Servlet之前对其进行访问,也可以在响应信息返回到客户端之前对其进行拦截
Filter接口方法:init、doFilter、destory
- init:初始化过滤器,初始化有时间限制,超时则初始化失败
- doFilter:在请求servlet之前处理数据,返回给服务器之前再处理数据
- destory:销毁filter链
Interceptor拦截器是AOP的一种实现策略,用于在某个方法或字段被访问前对它进行拦截,然后在其之前或之后加上某些操作
Interceptor接口方法:preHandler、postHandler、afterCompletion
- preHandler:controller调用之前,请求处理前的操作(权限等)
- postHandler:controller调用之后进行调用,视图渲染之前进行操作
- afterCompletion:最后执行、一般进行一些资源的清理工作
拦截器与过滤器的功能比较相似,我们接下来用两种方式实现日志功能
过滤器实现:
- 自定义过滤器实现Filter(servlet包下的)接口
- 写doFilter请求前后逻辑
- 自定义过滤器类上定义@WebFilter注解,说明需要拦截的请求地址以及此过滤器名称
- springboot启动类写注解自动扫描自定义过滤器类
@Slf4j
@WebFilter(urlPatterns = "/*" ,filterName = "logFilter")
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
long start = System.currentTimeMillis();
filterChain.doFilter(servletRequest,servletResponse);
log.info("LogFilter Print Log: {} -> {}",
((HttpServletRequest) servletRequest).getRequestURI(),
System.currentTimeMillis() - start);
}
@Override
public void destroy() {
}
}
@SpringBootApplication
@ServletComponentScan("包名")
public class TestMainApplication {}
拦截器实现:
- 自定义拦截器实现HandlerInterceptor接口
- 写PreHandler和PostHandler实现逻辑
- 定义拦截器适配器实现WebMvc配置接口,把自定义拦截器配置给mvc
@Component
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
long start = System.currentTimeMillis();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
start = System.currentTimeMillis();
//((HandlerMethod) handler) 强转成调用方法的对象
log.info("logInterceptor ClassName preHandle:{}", ((HandlerMethod) handler).getBean().getClass().getName());
log.info("logInterceptor MethodName preHandle:{}", ((HandlerMethod) handler).getMethod().getName());
return true;//这里一定要返回true否则方法不会向下执行
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
log.info("logInterceptor print Log postHandle:{} -》 {}",
request.getRequestURI(),
System.currentTimeMillis() - start);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
//链式返回true的执行
}
}
@Component
@Configuration
public class WebInterceptorAdaptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//order后面的数字越小 越先被执行
registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**").order(0);
}
}
拦截器执行结果:
当多个拦截器时preHandler与postHandler执行顺序:preHandler按顺序执行,postHandler后面的拦截器先执行。
@Component
@Slf4j
public class UpdateLogIntercepter implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
request.setAttribute("startTime",System.currentTimeMillis());
log.info("UpdatelogInterceptor Log preHandle:{}",
request.getAttribute("startTime") );
return true;//这里一定要返回true否则方法不会向下执行
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
log.info("UpdatelogInterceptor print Log postHandle:{} -》 {}",
request.getRequestURI(),
System.currentTimeMillis() - (long) request.getAttribute("startTime") );
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
//链式返回true的执行
}
}
@Component
@Configuration
public class WebInterceptorAdaptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//order后面的数字越小 越先被执行
registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**").order(0);
registry.addInterceptor(new UpdateLogIntercepter()).addPathPatterns("/**").order(1);
}
}
多个拦截器执行结果:
总结参考
过滤器(Filter):它依赖于servlet容器。在实现上,基于函数回调,它可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的,是用来做一些过滤操作,获取我们想要获取的数据,比如:在Javaweb中,对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者Controller进行业务逻辑操作。通常用的场景是:在过滤器中修改字符编码(CharacterEncodingFilter)、在过滤器中修改HttpServletRequest的一些参数(XSSFilter(自定义过滤器)),如:过滤低俗文字、危险字符等。
拦截器(Interceptor):它依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上,基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在service或者一个方法前,调用一个方法,或者在方法后,调用一个方法,比如动态代理就是拦截器的简单实现,在调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在调用方法后打印出字符串,甚至在抛出异常的时候做业务逻辑的操作。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。
拦截器过滤器中使用输入输出流的坑
输入流标识从一个源读取数据;输出流标识向一个目标写数据:A文件→输入流读取→输出流输出→B文件
在过滤器、拦截器中对HTTP请求中的数据做校验、审计是非常常见的需求,如果是json数据,我们就需要读取输入流。奇怪的是,读取了Request的输入流之后,请求数据就不见了
于输入流/输出流,你需要知道的两个坑:
- Request的 getInputStream()和getReader()都只能使用一次
- Request的getInputStream()、getReader(). getParameter()方法互斥,也就是使用了其中一个,再使用另外的两个,是获取不到数据的
- Response也是一样的,与Request几乎是一样的
解决方法:使用 HttpServletRequestWrapper+Filter解决输入流不能重复读取问题
例子:当在拦截器中使用了输入流读取数据,controller中就获取不到输入流了
public class RequestParseUtils {
/**当前请求类型是否是rest的传输json数据请求*/
public static boolean isJson(HttpServletRequest request){
if (request.getContentType() != null &&
(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) ||
(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8)))){
return true;
}
return false;
}
/**输入流转换成json字符串*/
public static String getBodyString(final ServletRequest request) throws IOException {
return inputStream2String(request.getInputStream());
}
private static String inputStream2String(InputStream inputStream){
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
String line;
while ((line = reader.readLine()) != null){
sb.append(line);
}
}catch (IOException e){
throw new RuntimeException();
}
return sb.toString();
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/get")
public User getTest(@RequestBody(required = false) User user){
if (user != null){//会一直都是null,因为流已经被拦截器读取
return user;
}
return new User(-1L, "牛牛", 1);
}
}
@Slf4j
@Component
//记得配置拦截器注册
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (RequestParseUtils.isJson(request)){
String bodyString = RequestParseUtils.getBodyString(request);
User user;
user = new ObjectMapper().readValue(bodyString, User.class);
if (user != null){
log.info("HandlerInterceptor process user : {}", user);
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
解决办法代码实现:
public class RequestWapper extends HttpServletRequestWrapper {
//每次读取后保存输入流数据
private final byte[] body;
public RequestWapper(HttpServletRequest request) throws IOException{
super(request);
body = RequestParseUtils.getBodyString(request).getBytes(Charset.defaultCharset());
}
//重写以下两个会导致只能读取一次输入流的方法
@Override
public BufferedReader getReader() throws IOException {
//可以多次读取
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
//不是使用原有的getInputStream方法,而是读取我们定义的body
ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
@Slf4j
@WebFilter(urlPatterns = "/*" , filterName = "RequestWrapperFilter")
public class RequestWrapperFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("RequestWrapper Filter Replace InputStream: ");
//过滤器传递request时进行读取输入流的替换
RequestWapper requestWapper = new RequestWapper((HttpServletRequest) servletRequest);
filterChain.doFilter(requestWapper,servletResponse);
}
@Override
public void destroy() {
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/get")
public User getTest(@RequestBody(required = false) User user){
if (user != null){//可以获得传入对象了
return user;
}
return new User(-1L, "牛牛", 1);
}
}
SpringBoot
配置文件加载顺序
SpringBoot使用一个全局的配置文件,且配置文件名是固定的,可以使用application.properties格式,也可以使用application.yml格式。由于YAML 格式紧凑且可读性高,所以,SpringBoot 支持并推荐使用YAML格式的配置文件如果两种配置文件同时存在的时候,默认优先使用.properties配置文件(但是,千万不要这么做)
SpringBoot配置文件优先级加载顺序:
- file:./config/ →优先级最高(项目根路径下的config )
- file:./-优先级第二→(项目根路径下)
- classpath:/config/ →优先级第三(项目resources/config 下)
- classpath:/→优先级第四(项目resources根目录下)
高优先配置会覆盖低优先级配置,但是也可以互补
SpringBoot多环境配置:
- 多环境下使用spring.profile.active可以指定配置文件
- 使用占位符${spring.profiles.active},在启动命令中指定配置文件
application.yml
spring:
profiles:
# active: ${spring.profiles.active}
active: dev #dev配置文件生效
application-dev.yml
escape:
user:
name: qinyi-dev
age: 19
@Component
@Data
@ToString
@ConfigurationProperties(prefix = "escape.user")//自动映射配置文件数据
public class UserProperties {
private String name;
private Integer age;
}
@Autowired
UserProperties userProperties;
@Test
public void test8(){
System.out.println(userProperties.toString());
}
部署项目指定配置文件生效
application.yml
spring:
profiles:
active: ${spring.profiles.active}
pom.xml中需要配置springboot提供的编译maven的插件,这样就可以把项目打成一个jar包
<build>
<finalName>${artifactId}</finalName> //打包成的项目名字
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
修改idea里的terminal像git命令一样使用:
忽略测试用例然后打包项目:
通过idea的terminal进行java -jar的方式启动项目,指定dev配置文件生效:
不定时任务
SpringBoot中编写定时任务
两个注解:@EnableScheduling、@Scheduled
@Scheduled注解参数:fixedDelay、fixedRate、initialDelay、corn
@Component
@Slf4j
public class ScheduledTask {
//每一秒执行一次 一直在占用线程资源,导致第二个定时任务没法执行
@Scheduled(fixedRate = 1000)
public void oneTask() throws Exception{
log.info("oneTask : " + System.currentTimeMillis());
while (true){
Thread.sleep(2000);
log.info("oneTask process something");
}
}
//每一秒执行一次
@Scheduled(fixedRate = 1000)
public void twoTask(){
log.info("twoTask : " + System.currentTimeMillis());
}
}
启动类加上@EnableScheduling注解
但是启动后我们会发现只有oneTask执行了定时任务。原因在于如果不配置定时任务参数,springboot定时任务的线程池只有单线程
配置定时任务线程池:
- 配置文件中指定定时任务线程数 spring.task.scheduling.pool.size
- 自定义定时任务的线程池,编写ScheduleConfig
①
spring:
profiles:
active: dev
task:
scheduling:
pool:
size: 2
②
@Configuration
public class ScheduledConfig {
@Bean(name = "scheduledTask2")
public TaskScheduler scheduledTask(){
ThreadPoolTaskScheduler threadPoolTaskScheduler
= new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(2);
return threadPoolTaskScheduler;
}
}
异步任务
两个注解、一个规则:@EnablEAsync、@Async、有没有返回值(Future)
@EnablEAsync同上面的定时任务注解@EnableScheduled一样标志在启动类上表示允许异步方法执行
@Async同上面的@Scheduled一样,标注这是一个异步方法
对于异步任务,你需要思考:
- 默认情况下,异步任务的线程池是怎么配置的,是否满足我们的需求呢?
- 对于异步任务有返回的情况,如果获取结果超时怎么办呢﹖程序会一直hang在那里肯定不是个好的执行办法。解决:调用get(设置超时时间)方法
- 对于异步任务没有返回的情况(有返回的直接get 的时候就会抛异常),如果抛出了异常怎么处理呢?
@Slf4j
@Component
public class AsyncTask {
//普通
@Async
public void task01() throws Exception{
log.info("task01 start");
Thread.sleep(2000);
log.info("task01 end");
}
//有返回值
@Async
public Future<String> task02() throws Exception{
log.info("task02 start");
Thread.sleep(2000);
log.info("task02 end");
return new AsyncResult<>("task02");
}
//抛出异常
@Async
public void task03(){
log.info("task03");
throw new RuntimeException("task03 Exception");
}
}
启动类上标注:@EnableAsync
@Autowired
AsyncTask asyncTask;
@Test
public void test9() throws Exception{
asyncTask.task01();
Future<String> stringFuture = asyncTask.task02();
//stringFuture.get() 可以阻塞异步线程,直到有返回值
System.out.println("wait process02 complet, return :" + stringFuture.get() );
// asyncTask.task03();
}
自定义异步线程池:
@Slf4j
@Configuration
public class AsyncTaskConfig implements AsyncConfigurer {
//自定义线程池然后返回
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("liuru-task-");
executor.setCorePoolSize(2);
executor.setMaxPoolSize(8);
executor.setKeepAliveSeconds(5);
executor.setQueueCapacity(100);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//任务执行完成才销毁bean
executor.setWaitForTasksToCompleteOnShutdown(true);
//任务在线程里最多呆60s,然后自动销毁,防止队列一直阻塞
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
//③对于异步任务没有返回的情况(有返回的直接get 的时候就会抛异常) 默认处理方法
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncUncaughtExceptionHandler() {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
//异常 异步方法名 异步方法中的参数
log.error("发送报警邮件、报警短信。{},{}", method.getDeclaringClass().getName() + "." + method.getName(), Arrays.toString(objects));
}
};
}
}
@Autowired
AsyncTask asyncTask;
@Test
public void test9() throws Exception{
asyncTask.task01();
Future<String> stringFuture = asyncTask.task02();
//stringFuture.get() 可以阻塞异步线程,直到有返回值
System.out.println("wait process02 complet, return :" + stringFuture.get(1, TimeUnit.SECONDS) );
//因为这里只阻塞一秒,但是task02要执行两秒
//会抛出超时异常,所以可以捕获异常然后进行接下来的业务处理
}
}
@Test
public void test10() throws Exception{
asyncTask.task03();
Thread.sleep(3000L);//保证异步线程里的任务执行完
}
SpringBoot默认的Jackson
你需要知道Jackson和ObjectMapper是什么:
- Jackson可以轻松的将Java对象转换成json对象和xml文档,同样也可以将json、xml转换成Java 对象
- ObjectMapper类是Jackson 库的主要类。它称为ObjectMapper的原因是因为它将JSON映射到Java对象(反序列化),或将Java对象映射到JSON(序列化)
定义 ObjectMapperConfig的理由是什么:
- ObjectMapper是线程安全的,这样做方便直接注入使用
- ObjectMapper是线程安全的,应该尽可能的重用
- 统一化配置,在一个工程中更为合适
Jackson常用序列化注解:
- @JsonIgnore、@JsonIgnoreProperties:忽略序列化字段
- @JsonInclude:序列化包含字段
- @JsonProperty:为序列化的类定义名称
- @JsonFormat:时间序列化与反序列化
- @JsonSerialize:自定义序列化过程
注解使用实例:
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)//字段不为null的参与序列化
@JsonIgnoreProperties({"couponCode","id"})//忽略序列化字段
public class Coupon {
@JsonIgnore
private int id;
private String couponCode;
@JsonProperty("user")//序列化后的名称
private Long userId;
@JsonFormat(pattern = "hh:mm:ss")
private Date assignTime;
private CouponTemoplate temoplate;
private CouponStatus couponStatus;
//默认初始化
public static Coupon fake(){
return new Coupon(1,"1",1L,new Date(),
new CouponTemoplate("qinyi","logo"),CouponStatus.USEABLE);
}
}
@Getter
@AllArgsConstructor
public enum CouponStatus {
USEABLE,
USERD;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class CouponTemoplate{
private String name;
private String logo;
}
@Configuration
public class ObjectMapperConfig {
@Bean
@Primary
public ObjectMapper objectMapper(){
ObjectMapper objectMapper = new ObjectMapper();
//忽略json字符串中不识别的字段
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
return objectMapper;
}
}
@Autowired
private ObjectMapper objectMapper;
@Test
public void test11() throws Exception{
Coupon coupon = Coupon.fake();
coupon.setTemoplate(null);
System.out.println(objectMapper.writeValueAsString(coupon));
//{"user":1,"assignTime":"09:13:19","couponStatus":"USEABLE"}
}
对类进行@JsonSerializ自定义序列化:
@JsonSerialize(using = CouponSerialize.class)定义在需要序列化的类上
//自定义类序列化器
public class CouponSerialize extends JsonSerializer<Coupon> {
@Override
public void serialize(Coupon coupon, JsonGenerator generator, SerializerProvider serializerProvider) throws IOException {
//开始序列化
generator.writeStartObject();
generator.writeStringField("id",String.valueOf(coupon.getId()));
generator.writeStringField("assignTime",new SimpleDateFormat("hh:mm:ss").format(coupon.getAssignTime()));
generator.writeStringField("name",String.valueOf(coupon.getTemoplate().getName()));
//介绍
generator.writeEndObject();
}
}
@Autowired
private ObjectMapper objectMapper;
@Test
public void test11() throws Exception{
Coupon coupon = Coupon.fake();
System.out.println(objectMapper.writeValueAsString(coupon));
//{"id":"1","assignTime":"05:20:05","name":"qinyi"}
}
自定义反序列化:
@Autowired
private ObjectMapper objectMapper;
@Test
public void test11() throws Exception{
Coupon coupon = Coupon.fake();
System.out.println(objectMapper.writeValueAsString(coupon));
//{"id":"1","assignTime":"05:20:05","name":"qinyi"}
//反序列化
String jsonCoupon = "{\"id\":\"1\",\"assignTime\":\"05:20:05\",\"name\":\"qinyi\"}";
objectMapper.setDateFormat(new SimpleDateFormat("hh:mm:ss"));
Coupon coupon1 = objectMapper.readValue(jsonCoupon, Coupon.class);
System.out.println(coupon1);
}
Jackson 与fastjson的对比:
- 性能∶速度上两个类库差距不大,其中,Jackson 的 ObjectMapper '实例化较慢;而fastjson使用静态方法,速度更快
- 文档:Jackson有非常详细的文档和demo 说明; fastjson文档较少
- 可定制型:Jackson有灵活的 API,可以很容易进行扩展和定制;fastjson定制功能稍显简陋
Mysql
NULL字段
记住,任何场景下你都不应该考虑使用NULL
mysql中字段设置为NULL需关注的问题:
- NULL字段的长度不是0是NULL
- NULL参与的查询问题
- NULL对索引的影响
- NULL参与计算
- NULL参与聚合
- NULL参与排序
NULL字段查询的问题:
字段是否为NULL使用 where 字段 is null or 字段 is not null;
对于普通查询select * from user where name != 'xyz';
其实后面都默认补全了不为null的判断:select * from user where name != 'xyz' and name is not null;
NULL字段对索引的影响:
- 如果字段设置成唯一索引,但默认值是NULL则索引失效,因为存储了两个null值
- 当对加了索引的某列进行查询时,此列默认值时null,此列索引是不生效的,只有字段条件是is null才使用了索引
NULL字段值参与计算:
mysql规定无论null值与字符串连接还是和数字参与计算,结果都会为null
NULL参与聚合:
以此可见统计条数时,如果以null为默认值的字段为条件,记得加上where name != 'xyz' or name is null
SELECT count(*) FROM tbmp_release_fund WHERE invest_industry_code != 01 or invest_industry_code is null
NULL参与排序:asc排序排在最前,desc排序排在最后
准确设置数据类型
不再随意设置数据类型,不给未来留隐患
MySQL中定义了四类数据类型,且有不同的取值范围:
字符串:char、varchar、【建议超过500字符后使用:tinytext、text、mediumtext、longtext】
日期/时间:date、time、datetime、timestamp
数值: tinyint、int、bigint、float、double、decimal
二进制: tityblob、 blob、mediumblob、longblob
help ‘CHAR’ 可查看范围
数据类型选择与使用上的技巧与建议:
- 使用存储所需要的最小数据类型
- 选择简单的数据类型
- 存储小数直接选择decimal
- 尽量避免使用text和blob
如何正确使用索引
索引查询有个很重要的问题:当查询出的字段是表的索引字段,所有使索引失效的条件都不成立,因为mysql维护了一个覆盖索引的缘故
不使用索引的场景:
- 经常增删改查的列不要建索引,因为修改数据的同时,索引也是要修改的。
- 有大量重复的列不要创建索引。mysql查询结果要小于30%才会使用索引,不然会使用全表扫描。mysql优化器认为全表扫描的成本小于索引,所以放弃索引,这是很多情况下没使用索引的原因。下面会具体介绍。所以具有唯一性或者重复性很少的列建立索引会非常有效
- 表记录太少不要建立索引,只有当数据库已经有列足够多的数据时,它的性能才会有实际参考价值。只有当记录超过1000条数据时,数据总理也超过了mysql服务器上的内存总量时,数据库的性能测试结果才有意义。
以下通过desc显示出mysql执行的字段内容:
- id: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符.
- select_type: SELECT 查询的类型.
- table: 查询的是哪个表
- partitions: 匹配的分区
- type: join 类型
- possible_keys: 此次查询中可能选用的索引
- key: 此次查询中确切使用到的索引.
- ref: 哪个字段或常数与 key 一起被使用
- rows: 显示此查询一共扫描了多少行. 这个是一个估计值.
- filtered: 表示此查询条件所过滤的数据的百分比
- extra: 额外的信息
索引加的正确,但是没有写出适用索引的查询语句:
- 字符串类型在查询时没有使用引号,不会使用表索引(隐式类型转换、隐式字符编码转换)
select * from user where phone != 18302899999;
select * from user where phone != '18302899999';
- where 条件左边的(属性列)字段参与了函数或者数学运算,不会使用表索引
select * from user where phone * 2 = phone;
select * from user where concat(phone,',') = phone;
- LIKE操作中,like name ‘%aaa%’,则name索引会失效,但是like name ‘aaa%’是可以使用索引。
- where语句中使用 or,但是没有把or中所有字段加上索引。
- where语句中对字段表达式或函数操作,会使索引失效
- 组合索引,只要where条件中有组合索引中的第一个字段就肯定会使用索引,与索引顺序无关。单列索引和组合索引优先级问题:组合索引是优先于单列索引的
- where语句中使用Not In 会导致索引失效
- where 语句中使用 is null 或者 is not null,当查询量达到总表的30%以上时,索引就会失效。如果排序使用了索引,而select列未使用索引列,则该索引失效,这是因为优化器执行直接执行全表扫描速度更快。主键索引除外
mysql连接经常超时
show GLOBAL variables like '%interactive_timeout%' 交互式:navicat连接
show GLOBAL variables like '%wait_timeout%' 非交互式:jdbc连接
即使设置了,重启mysql后就会变为默认的
默认是八小时:
可以配置:autoReconnect
jdbc:mysql://127.0.0.1:3306/imooc_escape?useUnicode=true&characterEncoding=utf8
还可以修改mysql配置文件:使修改时间永久生效
sudo vim /etc/my.cnf
记录慢SQL
怎样配置MySQL定位执行“慢”的查询,修改以下参数
- slow_query_log: 标记慢查询日志是否开启的参数,默认是OFF,即不开启
- slow_query_log_file: 标记存放慢查询日志文件的完整路径(注意权限问题)
- long_query_time: 控制慢查询的时间阈值参数
- log_queries_not_using_indexes: 标识是否记录未使用索引的查询,默认是关闭
vim /etc/my.cnf 全局配置
可以在语句中使用desc和explain来分析查询。也可以使用mysqldumpslow 工具解读慢查询日志。
分库分表
业务发展过程中遇到了哪些问题?
- 随着业务的增长,数据量急剧膨胀,查询、迁移都会变得非常麻烦,最经典的例子是用户表和订单表
- 表设计的不合理,在使用过程中发现问题(列太多,且99%的查询大部分的列不会被用到)
可以考虑分库分表解决这些问题
分表策略有:垂直切分和水平切分
- 垂直切分常用与常用字段与不常用字段的切分
- 水平切分常用于数据表量大的切分,切分规则常是按时间或者自增id或者哈希取模切分
分库分表引发的问题:
全局主键问题 解决方案:UUID、额外的自增表、分布式全局唯一ID生成算法
事务一致性问题 解决方案:XA协议、2PC两阶段提交、3PC三极端提交
关联查询(JOIN)问题 解决方案:字段冗余设计、数据组装、拆分查询
一般都是先垂直、再水平区分,但是垂直区分一定要让分开的各个表有关联