日常学习工作中的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流

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接口却报错原因?

  • 子类实现序列化接口,父类没有实现序列化但是有无参构造函数,子类就可以序列化
  • 类实现了序列化,类中的引用对象没实现序列化,此类进行序列化操作会报错
  • 在对象第一次序列化以后,无论后面如何改变这个对象,反序列化以后都是第一次保存的值(因为序列化时会去硬盘或者内存中查看是否有这个序列化码,有的话就不会覆盖了)

泛型初识

  1. 什么是泛型:泛型就是将固定的类型转换为动态的参数化类型
  2. 为什么要使用泛型:能够在不创建新类型的情况下,通过泛型指定的不同类型来控制形参具体的限制类型
  3. 泛型有泛型类、泛型接口、泛型方法三种
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
    }
}

泛型认识

  1. 泛型是先检查再编译
  2. 泛型不支持继承
  3. 泛型变量不能是基本数据类型
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,所以没有影响。但标准的引用类就显示出了是浅拷贝
}

如何实现深拷贝

  1. 引用类型也重写clone、然后重写主clone类的方法
  2. 序列化输入输出流
//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修饰即可

保证线程安全方法

  1. synchronized同步锁
  2. 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 )):

  1. AbortPolicy ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  2. CallerRunsPolicy ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
  3. DiscardPolicy ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
  4. 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管理

SpringBean自动命名后:
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出现空指针

  1. 没有将引入类交给spring管理,并且通过new来获取对象
  2. 把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异常
  1. 因为自动注入希望注入的类不为null,所以注入的bean没有实现类会报NoSuchBeanDefinitionException
public interface ITemplateService {
    void print();
}
@Autowired
ITemplateService iTemplateService;
@Test
public void test4(){
    iTemplateService.print();
}
//解决办法:注入时允许实现为null
@Autowired(required = false)
ITemplateService iTemplateService;
  1. 引入的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,也未回滚原因:

  1. trycatch捕获了异常
  2. 抛出了非RuntimeException异常,即抛出了方法也需要throws出去的异常
  3. 当前方法没有标注事务注解,调用的方法有标注,此情况下也不会回滚
  4. @Transactional只能标注在public方法上
  5. 数据库本身定义的类型就不支持事务,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:最后执行、一般进行一些资源的清理工作

拦截器与过滤器的功能比较相似,我们接下来用两种方式实现日志功能
过滤器实现:

  1. 自定义过滤器实现Filter(servlet包下的)接口
  2. 写doFilter请求前后逻辑
  3. 自定义过滤器类上定义@WebFilter注解,说明需要拦截的请求地址以及此过滤器名称
  4. 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 {}

拦截器实现:

  1. 自定义拦截器实现HandlerInterceptor接口
  2. 写PreHandler和PostHandler实现逻辑
  3. 定义拦截器适配器实现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配置文件优先级加载顺序:

  1. file:./config/ →优先级最高(项目根路径下的config )
  2. file:./-优先级第二→(项目根路径下)
  3. classpath:/config/ →优先级第三(项目resources/config 下)
  4. 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定时任务的线程池只有单线程
配置定时任务线程池:

  1. 配置文件中指定定时任务线程数 spring.task.scheduling.pool.size
  2. 自定义定时任务的线程池,编写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一样,标注这是一个异步方法

对于异步任务,你需要思考:

  1. 默认情况下,异步任务的线程池是怎么配置的,是否满足我们的需求呢?
  2. 对于异步任务有返回的情况,如果获取结果超时怎么办呢﹖程序会一直hang在那里肯定不是个好的执行办法。解决:调用get(设置超时时间)方法
  3. 对于异步任务没有返回的情况(有返回的直接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的对比:

  1. 性能∶速度上两个类库差距不大,其中,Jackson 的 ObjectMapper '实例化较慢;而fastjson使用静态方法,速度更快
  2. 文档:Jackson有非常详细的文档和demo 说明; fastjson文档较少
  3. 可定制型:Jackson有灵活的 API,可以很容易进行扩展和定制;fastjson定制功能稍显简陋

Mysql

NULL字段

记住,任何场景下你都不应该考虑使用NULL
mysql中字段设置为NULL需关注的问题:

  1. NULL字段的长度不是0是NULL
  2. NULL参与的查询问题
  3. NULL对索引的影响
  4. NULL参与计算
  5. NULL参与聚合
  6. 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: 额外的信息

索引加的正确,但是没有写出适用索引的查询语句:

  1. 字符串类型在查询时没有使用引号,不会使用表索引(隐式类型转换、隐式字符编码转换)
select * from user where phone != 18302899999;
select * from user where phone != '18302899999';
  1. where 条件左边的(属性列)字段参与了函数或者数学运算,不会使用表索引
select * from user where phone * 2 = phone;
select * from user where concat(phone,',') = phone;
  1. LIKE操作中,like name ‘%aaa%’,则name索引会失效,但是like name ‘aaa%’是可以使用索引。
  2. where语句中使用 or,但是没有把or中所有字段加上索引。
  3. where语句中对字段表达式或函数操作,会使索引失效
  4. 组合索引,只要where条件中有组合索引中的第一个字段就肯定会使用索引,与索引顺序无关。单列索引和组合索引优先级问题:组合索引是优先于单列索引的
  5. where语句中使用Not In 会导致索引失效
  6. 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 工具解读慢查询日志。

分库分表

业务发展过程中遇到了哪些问题?

  1. 随着业务的增长,数据量急剧膨胀,查询、迁移都会变得非常麻烦,最经典的例子是用户表和订单表
  2. 表设计的不合理,在使用过程中发现问题(列太多,且99%的查询大部分的列不会被用到)
    可以考虑分库分表解决这些问题

分表策略有:垂直切分和水平切分

  1. 垂直切分常用与常用字段与不常用字段的切分
  2. 水平切分常用于数据表量大的切分,切分规则常是按时间或者自增id或者哈希取模切分

分库分表引发的问题:
全局主键问题 解决方案:UUID、额外的自增表、分布式全局唯一ID生成算法
事务一致性问题 解决方案:XA协议、2PC两阶段提交、3PC三极端提交
关联查询(JOIN)问题 解决方案:字段冗余设计、数据组装、拆分查询

一般都是先垂直、再水平区分,但是垂直区分一定要让分开的各个表有关联

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值