这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

Java



public void b() {
    int a=3;
    int b=4;

    a=a^b;
    b=b^a;//b=b^(a^b)         ->b=a
    a=a^b;//a=(a^b)^(b^a)    ->a=b
    System.out.println(a);//输出4
    System.out.println(b);//输出3
}

1 - Java最佳实践

Java最佳实践。测试案例均通过JDK21。有些代码在JDK8中报错需要调整

1.1 - AES加解密

package cn.anzhongwei.lean.demo.encryption;

import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;


/**
 * AES加密类
 *
 */
public class AESUtils {
    /**
     * 密钥算法
     */
    private static final String KEY_ALGORITHM = "AES";

    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    /**
     * 初始化密钥
     *
     * @return byte[] 密钥
     * @throws Exception
     */
    public static byte[] initSecretKey() {
        // 返回生成指定算法的秘密密钥的 KeyGenerator 对象
        KeyGenerator kg = null;
        try {
            kg = KeyGenerator.getInstance(KEY_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return new byte[0];
        }
        // 初始化此密钥生成器,使其具有确定的密钥大小
        // AES 要求密钥长度为 128
        kg.init(128);
        // 生成一个密钥
        SecretKey secretKey = kg.generateKey();
        return secretKey.getEncoded();
    }

    /**
     * 转换密钥
     *
     * @param key
     *            二进制密钥
     * @return 密钥
     */
    public static Key toKey(byte[] key) {
        // 生成密钥
        return new SecretKeySpec(key, KEY_ALGORITHM);
    }

    /**
     * 加密
     *
     * @param data
     *            待加密数据
     * @param key
     *            密钥
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data, Key key) throws Exception {
        return encrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * 加密
     *
     * @param data
     *            待加密数据
     * @param key
     *            二进制密钥
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
        return encrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * 加密
     *
     * @param data
     *            待加密数据
     * @param key
     *            二进制密钥
     * @param cipherAlgorithm
     *            加密算法/工作模式/填充方式
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data, byte[] key, String cipherAlgorithm) throws Exception {
        // 还原密钥
        Key k = toKey(key);
        return encrypt(data, k, cipherAlgorithm);
    }

    /**
     * 加密
     *
     * @param data
     *            待加密数据
     * @param key
     *            密钥
     * @param cipherAlgorithm
     *            加密算法/工作模式/填充方式
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data, Key key, String cipherAlgorithm) throws Exception {
        // 实例化
        Cipher cipher = Cipher.getInstance(cipherAlgorithm);
        // 使用密钥初始化,设置为加密模式
        cipher.init(Cipher.ENCRYPT_MODE, key);
        // 执行操作
        return cipher.doFinal(data);
    }

    /**
     * 解密
     *
     * @param data
     *            待解密数据
     * @param key
     *            二进制密钥
     * @return byte[] 解密数据
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
        return decrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * 解密
     *
     * @param data
     *            待解密数据
     * @param key
     *            密钥
     * @return byte[] 解密数据
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data, Key key) throws Exception {
        return decrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * 解密
     *
     * @param data
     *            待解密数据
     * @param key
     *            二进制密钥
     * @param cipherAlgorithm
     *            加密算法/工作模式/填充方式
     * @return byte[] 解密数据
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data, byte[] key, String cipherAlgorithm) throws Exception {
        // 还原密钥
        Key k = toKey(key);
        return decrypt(data, k, cipherAlgorithm);
    }

    /**
     * 解密
     *
     * @param data
     *            待解密数据
     * @param key
     *            密钥
     * @param cipherAlgorithm
     *            加密算法/工作模式/填充方式
     * @return byte[] 解密数据
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data, Key key, String cipherAlgorithm) throws Exception {
        // 实例化
        Cipher cipher = Cipher.getInstance(cipherAlgorithm);
        // 使用密钥初始化,设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, key);
        // 执行操作
        return cipher.doFinal(data);
    }

    public static String showByteArray(byte[] data) {
        if (null == data) {
            return null;
        }
        StringBuilder sb = new StringBuilder("{");
        for (byte b : data) {
            sb.append(b).append(",");
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append("}");
        return sb.toString();
    }

    /**
     * 将16进制转换为二进制
     *
     * @param hexStr
     * @return
     */
    public static byte[] parseHexStr2Byte(String hexStr) {

        if (hexStr.length() < 1)
            return null;
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }

    /**
     * 将二进制转换成16进制
     *
     * @param buf
     * @return
     */
    public static String parseByte2HexStr(byte buf[]) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < buf.length; i++) {
            String hex = Integer.toHexString(buf[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    /**
     * @param str
     * @param key
     * @return
     * @throws Exception
     */
    public static String aesEncrypt(String str, String key) throws Exception {
        if (str == null || key == null)
            return null;
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "AES"));
        byte[] bytes = cipher.doFinal(str.getBytes("utf-8"));
        return Base64.getEncoder().encodeToString(bytes);
    }

    public static String aesDecrypt(String str, String key) throws Exception {
        if (str == null || key == null)
            return null;
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "AES"));
        byte[] bytes = Base64.getDecoder().decode(str);
        bytes = cipher.doFinal(bytes);
        return new String(bytes, "utf-8");
    }

    public static void main(String[] args) throws Exception {



        // 指定key
        String base64Str1 = "cmVmb3JtZXJyZWZvcm1lcg==";
        byte[] bs = Base64.getDecoder().decode(base64Str1);
        System.out.println("base64Str1解密:"+ Arrays.toString(bs));
        System.out.println("base64Str1解密:" + showByteArray(Base64.getDecoder().decode(base64Str1)));
        Key key1 = toKey(Base64.getDecoder().decode(base64Str1));

        String data1 = "{\"aaa\":\"bbb\"}";
        System.out.println("明文:" + data1);

        byte[] encryptData1 = encrypt(data1.getBytes(), key1);
        String encryptStr1=parseByte2HexStr(encryptData1);
        System.out.println("加密后数据1: byte[]:" + showByteArray(encryptData1));
        System.out.println("加密后数据1: Byte2HexStr:" + encryptStr1);
        System.out.println();

        byte[] encryptStrByte1 = parseHexStr2Byte(encryptStr1);
        byte[] decryptData1 = decrypt(encryptStrByte1, key1);
        System.out.println("解密后数据: byte[]:" + showByteArray(decryptData1));
        System.out.println("解密后数据: string:" + new String(decryptData1));


        System.out.println("--------------------------------------------------------");


        byte[] initKey = initSecretKey();
        System.out.println("key:" + Arrays.toString(Base64.getEncoder().encode(initKey)));
        System.out.println("key:" + showByteArray(initKey));
        Key key2 = toKey(initKey);

        String data2 = "{\"ccc\":\"ddd\"}";
        System.out.println("明文:" + data2);

        byte[] encryptData2 = encrypt(data2.getBytes(), key2);
        String encryptStr2=parseByte2HexStr(encryptData2);
        System.out.println("加密后数据1: byte[]:" + showByteArray(encryptData2));
        System.out.println("加密后数据1: Byte2HexStr:" + encryptStr2);
        System.out.println();

        byte[] encryptStrByte2 = parseHexStr2Byte(encryptStr2);
        byte[] decryptData2 = decrypt(encryptStrByte2, key2);
        System.out.println("解密后数据: byte[]:" + showByteArray(decryptData2));
        System.out.println("解密后数据: string:" + new String(decryptData2));

    }
}

1.2 - fastjson

这里主要是Map和JSONObject的转换
package cn.anzhongwei.lean.demo.alibabafastjsondemo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import java.util.Map;

public class JsonToMapDemo1 {
    public static void main(String[] args){

        String str = "{\"0\":\"zhangsan\",\"1\":\"lisi\",\"2\":\"wangwu\",\"3\":\"maliu\"}";
        //第一种方式
        Map maps = (Map) JSON.parse(str);
        System.out.println("这个是用JSON类来解析JSON字符串!!!");
        for (Object map : maps.entrySet()){
            System.out.println(((Map.Entry)map).getKey()+"     " + ((Map.Entry)map).getValue());
        }
        //第二种方式
        Map mapTypes = JSON.parseObject(str);
        System.out.println("这个是用JSON类的parseObject来解析JSON字符串!!!");
        for (Object obj : mapTypes.keySet()){
            System.out.println("key为:"+obj+"值为:"+mapTypes.get(obj));
        }
        //第三种方式
        Map mapType = JSON.parseObject(str,Map.class);
        System.out.println("这个是用JSON类,指定解析类型,来解析JSON字符串!!!");
        for (Object obj : mapType.keySet()){
            System.out.println("key为:"+obj+"值为:"+mapType.get(obj));
        }
        //第四种方式

        Map json = (Map) JSONObject.parse(str);
        System.out.println("这个是用JSONObject类的parse方法来解析JSON字符串!!!");
        for (Object map : json.entrySet()){
            System.out.println(((Map.Entry)map).getKey()+"  "+((Map.Entry)map).getValue());
        }
        //第五种方式

        JSONObject jsonObject = JSONObject.parseObject(str);
        System.out.println("这个是用JSONObject的parseObject方法来解析JSON字符串!!!");
        for (Object map : json.entrySet()){
            System.out.println(((Map.Entry)map).getKey()+"  "+((Map.Entry)map).getValue());
        }
        //第六种方式

        Map mapObj = JSONObject.parseObject(str,Map.class);
        System.out.println("这个是用JSONObject的parseObject方法并执行返回类型来解析JSON字符串!!!");
        for (Object map: json.entrySet()){
            System.out.println(((Map.Entry)map).getKey()+"  "+((Map.Entry)map).getValue());
        }
        String strArr = "{{\"0\":\"zhangsan\",\"1\":\"lisi\",\"2\":\"wangwu\",\"3\":\"maliu\"}," +
                "{\"00\":\"zhangsan\",\"11\":\"lisi\",\"22\":\"wangwu\",\"33\":\"maliu\"}}";
        // JSONArray.parse()
        System.out.println(json);

    }
}

最终输出结果为:

这个是用JSON类来解析JSON字符串!!!
0     zhangsan
1     lisi
2     wangwu
3     maliu
这个是用JSON类的parseObject来解析JSON字符串!!!
key为:0值为:zhangsan
key为:1值为:lisi
key为:2值为:wangwu
key为:3值为:maliu
这个是用JSON类,指定解析类型,来解析JSON字符串!!!
key为:0值为:zhangsan
key为:1值为:lisi
key为:2值为:wangwu
key为:3值为:maliu
这个是用JSONObject类的parse方法来解析JSON字符串!!!
0  zhangsan
1  lisi
2  wangwu
3  maliu
这个是用JSONObject的parseObject方法来解析JSON字符串!!!
0  zhangsan
1  lisi
2  wangwu
3  maliu
这个是用JSONObject的parseObject方法并执行返回类型来解析JSON字符串!!!
0  zhangsan
1  lisi
2  wangwu
3  maliu
{"0":"zhangsan","1":"lisi","2":"wangwu","3":"maliu"}

1.3 - Integer比较

package cn.anzhongwei.lean.demo.intandinteger;


// java 9 以后 此初始化已经标记为废弃,推荐使用 Integer.valueOf("100");
public class IntegerTest {
    public static void main(String[] args) {
        int a = 100;

        Integer b = 100;
        if ( a == b ) {
            //原因是由于此区间的只都是由IntegerCache.cache而来,具体查看Integer.valueOf(n)方法
            //其中IntegerCache.cache  static final Integer cache[];
            System.out.println("-128 - 127 之间的int类型是在常量池中, ");
        }


        // 但是  Integer.valueOf("100");
        Integer c = Integer.valueOf("300");
        Integer d = Integer.valueOf("300");
        if ( c != d ) {
            System.out.println("Integer.valueOf(\"100\") 初始化的127以上的数字是放到推空间的");
        }


        Integer e =  Integer.valueOf("100");
        Integer f =  Integer.valueOf("100");
        if ( e == f ) {
            System.out.println("Integer.valueOf(\"100\") 初始化的127以内的还是从常量池中获取");
        }

        Integer g = Integer.valueOf("400");
        int h = 400;
        if ( g == h ) {
            System.out.println("Integer会向下转型");
        }

    }
}

1.4 - Java的参数是值传递

class User

package cn.anzhongwei.lean.demo.java传参;

public class User {
    private String name;
    private String Gender;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGender() {
        return Gender;
    }

    public void setGender(String gender) {
        Gender = gender;
    }
}
package cn.anzhongwei.lean.demo.java传参;

/**
 * 要理解java的传参
 * 结果:
 主类中实例化user1-hash:cn.anzhongwei.demo.java传参.User@4d7e1886
 主类中实例化参数:Name=Hollis
 方法中接受到参数user-hash:cn.anzhongwei.demo.java传参.User@4d7e1886  // 与main函数同一个对象
 方法中重新实例化参数user-hash:cn.anzhongwei.demo.java传参.User@3cd1a2f1 // 产生新对象,但是只在局部有效
 方法中重新实例化参数:Name=hollischuang
 经过方法后的user1-hashcn.anzhongwei.demo.java传参.User@4d7e1886  //user1并没有因为在函数中局部参数实例化而发生变化
 经过方法后的参数xiaoming //实例属性会被函数中的更改而发生变化
 */
public class PassOn {
    public void pass(User user) {
        System.out.println("方法中接受到参数user-hash:"+user);
        user.setName("xiaoming");


        user = new User();
        user.setName("hollischuang");
        System.out.println("方法中重新实例化参数user-hash:"+user);
        System.out.println("方法中重新实例化参数:Name="+user.getName());
    }
    public static void main(String[] args) {
        PassOn pt = new PassOn();

        User user1 = new User();
        System.out.println("主类中实例化user1-hash:"+user1);
        user1.setName("Hollis");
        user1.setGender("Male");
        System.out.println("主类中实例化参数:Name="+user1.getName());
        pt.pass(user1);
        System.out.println("经过方法后的user1-hash" + user1);
        System.out.println("经过方法后的参数" + user1.getName());
    }

}

1.5 - Optional

package cn.anzhongwei.lean.demo.optional;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Optional;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    private Integer age;
    private Address address;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public Optional<String> optionalGetName() {
        return Optional.ofNullable(this.name);
    }
}
package cn.anzhongwei.lean.demo.optional;

import lombok.Data;

@Data
public class Address {
    private String city;
    private String addInfo;
}
package cn.anzhongwei.lean.demo.optional;

import org.junit.Test;

import java.util.*;
import java.util.stream.Collectors;

public class OptionalTest {

    /**
     * Optional(T value),empty(),of(T value),ofNullable(T value)
     * Optional(T value) 是private的属于内部方法
     */
    @Test
    public void test1() {
        //of(T value) 是实际上调用了Optional(T value) 而其内部的requireNonNull方法源码中判断value==null时会抛出空指针异常
        //在开发中如果不想隐藏空指针异常时 使用of函数, 不过这种情况几乎不用
        //Optional<Person> p1 = Optional.of(null);

        //所以如果要创建一个value为null的Optional对象则应该使用 empty()
        Optional<Person> pnull = Optional.empty();
        //而ofNullable(T value) 则是一个三元表达式  return value == null ? empty() : of(value);
        Optional<Person> p0 = Optional.ofNullable(null);
        Optional<Person> p1 = Optional.ofNullable(new Person("zhangs", 20));
    }

    /**
     * orElse(T other),orElseGet(Supplier other)和orElseThrow(Supplier exceptionSupplier)
     * 这三个函数放一组进行记忆,都是在构造函数传入的value值为null时,进行调用的。
     * orElse 和 orElseGet 没区别 可以认为时完全一样的代码
     *
     * orElseThrow 则会当value为空时抛出一个自定义异常
     */
    @Test
    public void test2() {
        Person p = null;
        //当p值不为null时,orElse函数依然会执行createUser()方法
        p = Optional.ofNullable(p).orElse(new Person("zhangsan", 20));
        System.out.println("调用orElse 当p为null时:"+p);
        //而当p不为空,则相当于使用get()方法
        p = Optional.ofNullable(p).orElse(new Person("王五", 60));
        System.out.println("调用orElse 此时p不为null时:"+p);
        //
        p = null;
        //重新赋值p=null
        System.out.println("重新赋值p=:"+p);
        p = Optional.ofNullable(p).orElseGet(() -> new Person("lisi", 30));
        System.out.println("orElseGet 当p为null时:"+p);
        p = Optional.ofNullable(p).orElseGet(() -> new Person("zhaoliu", 34));
        System.out.println("orElseGet 此时p不为null时:"+p);


    }
    @Test
    public void testOrElseThrow() {
        Person p = new Person("lisi", 30);
        p = Optional.ofNullable(p).orElseThrow(()-> new RuntimeException("用户不存在"));
        System.out.println("orElseThrow 此时p不为null时:"+p);
        p = null;
        p = Optional.ofNullable(p).orElseThrow(()-> new RuntimeException("用户不存在"));
    }

    /**
     * map(Function mapper)和flatMap(Function> mapper)
     * 这两个函数做的是转换值的操作
     */
    @Test
    public void test3() {
        Person p = new Person("lisi", 30);
        String name = Optional.ofNullable(p).map( person -> person.getName()).get();
        System.out.println("name="+name);

        String optionalName = Optional.ofNullable(p).flatMap(person -> person.optionalGetName()).get();
        System.out.println("optionalName="+optionalName);
    }

    /**
     * isPresent()和ifPresent(Consumer consumer)
     * isPresent即判断value值是否为空,而ifPresent就是在value值不为空时,做一些操作。
     */
    @Test
    public void test4() {
        Person p = null;
        Optional.ofNullable(p).ifPresent(person -> {

        });
    }

    /**
     * filter(Predicate predicate)
     */
    @Test
    public void test5() {
        Person p = null;
        //如果 p.name 长度小于6则返回, 否则返回EMPTY
        Optional<Person> p1 = Optional.ofNullable(p).filter(u -> u.getName().length()<6);
    }

    /**
     * 实战写法1
     */
    public String getCity(Person person)  throws Exception{
        if(person!=null){
            if(person.getAddress()!=null){
                Address address = person.getAddress();
                if(address.getCity()!=null){
                    return address.getCity();
                }
            }
        }
        throw new RuntimeException("取值错误");
    }
    public String getCity2(Person person) throws Exception{
        return Optional.ofNullable(person)
                .map(u-> u.getAddress())
                .map(a->a.getCity())
                .orElseThrow(()->new Exception("取值错误"));
    }

    /**
     * 实战写法2
     */
    public void dosomething(Person person) {
        if (Objects.nonNull(person)) {
            //......
        }
        Optional.ofNullable(person).ifPresent(person1 -> {
            //......
        });
    }

    /**
     * 实战写法3
     */
    public Person getUser1(Person user){
        if(user!=null){
            String name = user.getName();
            if(!"zhangsan".equals(name)){
                user = new Person();
                user.setName("zhangsan");
            }
        }else{
            user = new Person();
            user.setName("zhangsan");
        }
        return user;
    }
    public Person getUser2(Person user) {
        return Optional.ofNullable(user)
                .filter(u->"zhangsan".equals(u.getName()))
                .orElseGet(()-> {
                    Person user1 = new Person();
                    user1.setName("zhangsan");
                    return user1;
                });
    }



    public Person getUser3(Person user){
        if(user!=null){
            String name = user.getName();
            if(!"zhangsan".equals(name)){
                user.setName("zhangsan");
            }
        }else{
            user = new Person();
            user.setName("zhangsan");
        }
        return user;
    }
    public Person getUser4(Person user) {
        //这个逻辑一般来说不会这么写, 大多数情况下都是不为空且不为zhangsan时, 将name赋值成zhangsan,而不是重新实例化
        return Optional.ofNullable(user)
                .filter(u->"zhangsan".equals(u.getName()))
                .orElseGet(()-> {
                    if (Objects.nonNull(user)) {
                        user.setName("zhangsan");
                        return user;
                    } else {
                        Person user1 = new Person();
                        user1.setName("zhangsan");
                        return user1;
                    }


                });
    }
    @Test
    public void test6() {
        //getUser1 和 getUser2 只要name不等于 zhangsan, 就重新初始化
        Person p = new Person("wangwu", 26);
        Person p1 = getUser1(p);
        System.out.println("p1:"+p1);

        p = new Person("lisi", 23);
        Person p2 = getUser2(p);
        System.out.println("p2:"+p2);

        //getUser3 和 getUser4 name不等于 zhangsan, 只改变name=张三, 其他内容不变
        p = new Person("zhaosi", 26);
        Person p3 = getUser3(p);
        System.out.println("p3:"+p3);

        p = new Person("liuqi", 26);
        Person p4 = getUser4(p);
        System.out.println("p4:"+p4);
    }


    @Test
    public  void test7() {
        List<Person> personList = new ArrayList<>();
        Person p1 = new Person("zhangsan", 22);personList.add(p1);
        Person p2 = new Person("zhaosi", 29);personList.add(p2);
        Person p3 = new Person("wangwu", 22);personList.add(p3);
        Person p4 = new Person("lisen", 35);personList.add(p4);
        Person p5 = new Person("liuming", 248);personList.add(p5);

        Optional<Person> personOptional = personList.stream().filter(p -> p.getAge() > 22).findFirst();
        Person pp1 = personOptional.orElseGet(() -> personList.get(0));
        System.out.println(pp1);
        Person pp2 = personOptional.orElse(null);
        System.out.println(pp2);

        List<Person> personList2 = Optional.ofNullable(personList).orElseGet(ArrayList::new);
        System.out.println(personList2);

        Map<String, List<Person>> pMap = new HashMap<>();
//        List<Person> personList3 = Optional.ofNullable(pMap.get("随便写"))
//                //这种写法主要为了后面stream()的流式处理方式不会有空指针
//                //这样会空指针
//                .get()
//                .stream().collect(Collectors.toList());
//        System.out.println(personList3);

        List<Person> personList4 = Optional.ofNullable(pMap.get("随便写"))
                //这种写法主要为了后面stream()的流式处理方式不会有空指针
                .orElseGet(ArrayList::new)
                .stream().collect(Collectors.toList());
        System.out.println(personList4);
    }


}

1.6 - PDF转图片

package cn.anzhongwei.lean.demo.pdftoimg;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class PdfUtil {
    public static void main(String[] args) {
        String source = "C:\\Home\\22.pdf";
        String desFileName = "invoice";
        String desFilePath = "C:\\Home\\ddd";
        String imageType = "png";
        Long t1 = System.currentTimeMillis();
        List<String> pair = PdfUtil.pdfToImg(source, desFilePath, desFileName, imageType);
        Long t2 = System.currentTimeMillis();
        System.out.println(t2-t1);
        pair.forEach(System.out::println);


    }

    public static List<String> pdfToImg(String source, String desFilePath, String desFileName, String imageType) {
        //通过给定的源路径名字符串创建一个File实例
        File file = new File(source);
        if (!file.exists()) {
            throw new RuntimeException("文件不存在,无法转化");
        }
        //目录不存在则创建目录
        File destination = new File(desFilePath);
        if (!destination.exists()) {
            boolean flag = destination.mkdirs();
            System.out.println("创建文件夹结果:" + flag);
        }
        PDDocument doc = null;
        try {
            //加载PDF文件
            doc = PDDocument.load(file);
            PDFRenderer renderer = new PDFRenderer(doc);
            //获取PDF文档的页数
            int pageCount = doc.getNumberOfPages();
            System.out.println("文档一共" + pageCount + "页");
            List<String> fileList = new ArrayList<>();
            for (int i = 0; i < pageCount; i++) {
                //只有一页的时候文件名为传入的文件名,大于一页的文件名为:文件名_自增加数字(从1开始)
                String realFileName = pageCount > 1 ? desFileName + "_" + (i + 1) : desFileName;
                //每一页通过分辨率和颜色值进行转化
                BufferedImage bufferedImage = renderer.renderImageWithDPI(i, 96*2, ImageType.RGB);
                String filePath = desFilePath + File.separator + realFileName + "." + imageType;
                //写入文件
                ImageIO.write(bufferedImage, imageType, new File(filePath));
                //文件名存入list
                fileList.add(filePath);
            }
            return fileList;
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("PDF转化图片异常,无法转化");
        } finally {
            try {
                if (doc != null) {
                    doc.close();
                }
            } catch (IOException e) {
                System.out.println("关闭文档失败");
                e.printStackTrace();
            }
        }

    }
}

1.7 - Serializable序列化

  1. Person
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;

import java.io.Serializable;

@Data
@ToString
@AllArgsConstructor
public class Person implements Serializable {
    private String name;
    private int age;
}
  1. Teacher
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;

import java.io.Serializable;

@Data
@ToString
@AllArgsConstructor
public class Teacher implements Serializable {
    private String name;
    private Person person;
}
  1. 写序列化到文件
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

// TODO 还没测试成功
public class WriteObject {
    public static void main(String[] args) {
//        try (//创建一个ObjectOutputStream输出流
//             ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Home\\teacher.txt"))) {
//            //将对象序列化到文件s
//            Person person = new Person("9龙", 23);
//            Teacher teacher = new Teacher("路飞", person);
//            oos.writeObject(teacher);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }

        try (//创建一个ObjectOutputStream输出流
             ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\teacher2.txt"))) {
            //将对象序列化到文件s
            Person person = new Person("路飞", 20);
            Teacher t1 = new Teacher("雷利", person);
            Teacher t2 = new Teacher("红发香克斯", person);
            //依次将4个对象写入输入流
            oos.writeObject(t1);
            oos.writeObject(t2);
            System.out.println(t2.hashCode());
            oos.writeObject(person);
            oos.writeObject(t2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 从文件读序列化并实例化
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class ReadObject {
    public static void main(String[] args) {
//        try (//创建一个ObjectInputStream输入流
//             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Home\\teacher.txt"))) {
//            Teacher teacher = (Teacher) ois.readObject();
//            System.out.println(teacher);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }

        try (//创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\teacher2.txt"))) {
            Teacher t1 = (Teacher) ois.readObject();
            Teacher t2 = (Teacher) ois.readObject();
            Person p = (Person) ois.readObject();
            Teacher t3 = (Teacher) ois.readObject();
            System.out.println(t1 == t2);
            System.out.println(t1.getPerson() == p);
            System.out.println(t2.getPerson() == p);
            System.out.println(t2 == t3);
            System.out.println(t2.hashCode());
            System.out.println(t3.hashCode());
            System.out.println(t1.getPerson() == t2.getPerson());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1.8 - SimpleFTPClient


import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedList;
import java.util.List;


import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
<!--apacheftp-->
<!--导入commons-net依赖-->
<!--https://mvnrepository.com/artifact/commons-net/commons-net-->
<dependency>
    <groupId>commons-net</groupId>
    <artifactId>commons-net</artifactId>
    <version>3.6</version>
</dependency>

<!-- 引入Apache的commons-lang3包,方便操作字符串-->
<!--https://mvnrepository.com/artifact/org.apache.commons/commons-lang3-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8</version>

</dependency>

<!--引入Apachecommons-io包,方便操作文件-->
<!--https://mvnrepository.com/artifact/commons-io/commons-io-->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>
*/

public class SimpleFTPClient {
    private static final Logger logger = LoggerFactory.getLogger(SimpleFTPClient.class);
    private static FTPClient connectFtpServer(String url,String userName,String password){

        String  str = url.replaceFirst("ftp://", "").replaceFirst("FTP://", "");
        str=str.replaceAll("/+", "/");
        int endIndex = str.indexOf("/");
        str = str.substring(0,endIndex);
        url = str;//"ftp://"+str+"/";

        String[] addr = url.split(":");
        FTPClient ftpClient = new FTPClient();
        ftpClient.setConnectTimeout(1000*5);//设置连接超时时间
        // ftpClient.setControlEncoding("GBK");   
        ftpClient.setControlEncoding("utf-8");//设置ftp字符集
        ftpClient.enterLocalPassiveMode();//设置被动模式,文件传输端口设置
        try {
           
            ftpClient.connect(addr[0],Integer.parseInt(addr[1]));
           
            if(null==userName||"".equals(userName)){
                userName = "anonymous";
                password = "password";
            }
            boolean isLoginSuccess = ftpClient.login(userName,password);
            logger.info("isLoginSuccess="+isLoginSuccess);

            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);//设置文件传输模式为二进制,可以保证传输的内容不会被改变

            int replyCode = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(replyCode)){
                logger.error("connect ftp {} failed",url);
                ftpClient.abort();
                ftpClient.disconnect();
                return null;
            }
            logger.info("replyCode==========={}",replyCode);
        } catch (IOException e) {
            logger.error("connect fail ------->>>{}",e);
            return null;
        }
        return ftpClient;
}
    /**
     *
     * @param inputStream 待上传文件的输入流
     * @param originName 文件保存时的名字
     */
    public void uploadFile(String url,String userName,String password,InputStream inputStream, String originName){
        FTPClient ftpClient = connectFtpServer(url,userName,password);
        if (ftpClient == null){
            return;
        }
       
        /* try {
            ftpClient.changeWorkingDirectory(remoteDir);//进入到文件保存的目录
            Boolean isSuccess = ftpClient.storeFile(originName,inputStream);//保存文件
            if (!isSuccess){
                throw new BusinessException(ResponseCode.UPLOAD_FILE_FAIL_CODE,originName+"---》上传失败!");
            }
            logger.info("{}---》上传成功!",originName);
            ftpClient.logout();
        } catch (IOException e) {
            logger.error("{}---》上传失败!",originName);
            throw new BusinessException(ResponseCode.UPLOAD_FILE_FAIL_CODE,originName+"上传失败!");
        }finally {
            if (ftpClient.isConnected()){
                try {
                    ftpClient.disconnect();
                } catch (IOException e) {
                    logger.error("disconnect fail ------->>>{}",e.getCause());
                }
            }
        } */
    }
/**
     *  读ftp上的文件,并将其转换成base64
     * @param remoteFileName ftp服务器上的文件名
     * @return
     */
    public static String readFileToBase64(String url,String userName,String password){
        logger.info("url={}",url);
        FTPClient ftpClient = connectFtpServer(url,userName,password);
        if (ftpClient == null){
            return null;
        }
        logger.info("ftpClient={}",ftpClient.isAvailable()&&ftpClient.isConnected());
        String base64 = "";
        InputStream inputStream = null;
        ByteArrayOutputStream os=null;
        String remoteDir;
        String  str = url.replaceFirst("ftp://", "").replaceFirst("FTP://", "");
        str=str.replaceAll("/+", "/");
        int beginIndex = str.indexOf("/");
        int endIndex = str.lastIndexOf("/");
        remoteDir=str.substring(beginIndex,endIndex+1);
        logger.info("remoteDir={}",remoteDir);
        String remoteFileName = str.substring(endIndex+1);
        logger.info("remoteFileName={}",remoteFileName);
        try {
            boolean chDirRes = ftpClient.changeWorkingDirectory(remoteDir);
            logger.info("changeWorkingDirectory={}",chDirRes);
            ftpClient.enterLocalPassiveMode();
            FTPFile[] ftpFiles = ftpClient.listFiles(remoteDir);
            logger.info("ftpFiles.length={}",ftpFiles.length);
            Boolean flag = false;
            //遍历当前目录下的文件,判断要读取的文件是否在当前目录下
            for (FTPFile ftpFile:ftpFiles){
                if (ftpFile.getName().equals(remoteFileName)){
                    flag = true;
                    break;
                }
            }

            if (!flag){
                logger.error("directory:{}下没有 {}",remoteDir,remoteFileName);
                return null;
            }

           /*  // 下载ftp上指定文件到本地
            File localFile = new File("D:" + File.separator +"abc_"+ remoteFileName);
            boolean downloaded = ftpClient.retrieveFile(remoteFileName,
            new FileOutputStream(localFile));
           
            System.out.println(ftpClient.getReplyString()); */




            logger.info("开始获取文件流");
            //获取待读文件输入流
            inputStream = ftpClient.retrieveFileStream(remoteFileName); // remoteDir+remoteFileName

            // //inputStream.available() 获取返回在不阻塞的情况下能读取的字节数,正常情况是文件的大小
            logger.info("完成获取文件流");
            byte[] bytes = new byte[1024];
            os = new ByteArrayOutputStream();
            int len=-1;
            while( (len=inputStream.read(bytes))!=-1){//将文件数据读到字节数组中
                os.write(bytes, 0, len);
            }
            os.flush();
            byte[] fileBytes = os.toByteArray();
          
            /* File file = new File("d:/"+remoteFileName);
            OutputStream fos = new FileOutputStream(file);
            try {
                fos.write(fileBytes);
                fos.flush();
            } catch (Exception e) {
                //TODO: handle exception
            }
           finally{
            fos.close();
           } */
            

            int fileSize = fileBytes.length;
            logger.info("fileSize={}",fileSize);
            Base64.Encoder encoder = Base64.getEncoder();
            base64 = encoder.encodeToString(fileBytes);
            logger.debug("base64 img="+base64);
            logger.info("read file {} success",remoteFileName);

            if(!ftpClient.completePendingCommand()) {
                ftpClient.logout();
                ftpClient.disconnect();
                   logger.error("File transfer failed.");
                   return null;
            }

            ftpClient.logout();
        } catch (IOException e) {
            logger.error("read file fail ----->>>{}",e.getCause());
            return null;
        }finally {
            if (ftpClient.isConnected()){
                try {
                    ftpClient.disconnect();
                } catch (IOException e) {
                    logger.error("disconnect fail ------->>>{}",e.getCause());
                }
            }

            if (inputStream != null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    logger.error("inputStream close fail -------- {}",e.getCause());
                }
            }

            if (os != null){
                try {
                    os.close();
                } catch (IOException e) {
                    logger.error("os close fail -------- {}",e.getCause());
                }
            }

        }

        return base64;

 }
/**
     * 文件下载
     * @param remoteFileName ftp上的文件名
     * @param localFileName 本地文件名
     */
    public void download(String url,String userName,String password,String remoteFileName,String localFileName){
        FTPClient ftpClient = connectFtpServer(url,userName,password);
        if (ftpClient == null){
            return ;
        }

        OutputStream outputStream = null;

        /* try {
            ftpClient.changeWorkingDirectory(remoteDir);
            FTPFile[] ftpFiles = ftpClient.listFiles(remoteDir);
            Boolean flag = false;
            //遍历当前目录下的文件,判断是否存在待下载的文件
            for (FTPFile ftpFile:ftpFiles){
                if (ftpFile.getName().equals(remoteFileName)){
                    flag = true;
                    break;
                }
            }

            if (!flag){
                logger.error("directory:{}下没有 {}",remoteDir,remoteFileName);
                return ;
            }

            outputStream = new FileOutputStream(localDir+localFileName);//创建文件输出流

            Boolean isSuccess = ftpClient.retrieveFile(remoteFileName,outputStream); //下载文件
            if (!isSuccess){
                logger.error("download file 【{}】 fail",remoteFileName);
            }

            logger.info("download file success");
            ftpClient.logout();
        } catch (IOException e) {
            logger.error("download file 【{}】 fail ------->>>{}",remoteFileName,e.getCause());
        }finally {
            if (ftpClient.isConnected()){
                try {
                    ftpClient.disconnect();
                } catch (IOException e) {
                    logger.error("disconnect fail ------->>>{}",e.getCause());
                }
            }

            if (outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    logger.error("outputStream close fail ------->>>{}",e.getCause());
                }
            }
        } */
    }

    public static void main(String[] args) {
        // String ftpPath = "ftp://192.168.1.3:8010/recordsImg/2019-05-31/STRANGERBABY_1559265983072.jpg";
        // readFileToBase64(ftpPath,"","");
        String url="ftp://106.12.195.197:21/";
        FTPClient ftpClient = connectFtpServer(url,"myftp","123456");
        if (ftpClient == null){
            return ;
        }
        logger.info("ftpClient={}",ftpClient.isAvailable()&&ftpClient.isConnected());
        String base64 = "";
        InputStream inputStream = null;
        ByteArrayOutputStream os=null;
        String remoteDir;
        String  str = url.replaceFirst("ftp://", "").replaceFirst("FTP://", "");
        str=str.replaceAll("/+", "/");
        int beginIndex = str.indexOf("/");
        // int endIndex = str.lastIndexOf("/");
        
        // remoteDir=str.substring(beginIndex,endIndex+1);
        String remotePath =str.substring(beginIndex);
        logger.info("remotePath={}",remotePath);
        String status="nil";
        try {
            status=ftpClient.getStatus(remotePath);
        } catch (Exception e) {
            //TODO: handle exception
            e.printStackTrace();
        }
        logger.info("ftpClient.getStatus={}",status);
    
        try {
            // boolean chDirRes = ftpClient.changeWorkingDirectory(remoteDir);
            // logger.info("changeWorkingDirectory={}",chDirRes);
            // ftpClient.enterLocalPassiveMode();
           /*  FTPFile[] ftpFiles = ftpClient.listFiles(remoteDir);
            logger.info("ftpFiles.length={}",ftpFiles.length);
            Boolean flag = false;
            //遍历当前目录下的文件,判断要读取的文件是否在当前目录下
            for (FTPFile ftpFile:ftpFiles){
                logger.info("ftpFiles------{}",ftpFile.getName());
            } */
        //    boolean renameRes = ftpClient.rename("/imgfacebak/bb1.png", "/test/a.png");
        //    logger.info("renameRes={}",renameRes);
            // if(!ftpClient.completePendingCommand()) {
            //     ftpClient.logout();
            //     ftpClient.disconnect();
            //        logger.error("File transfer failed.");
            //        return ;
            // }

            ftpClient.logout();
          
        } catch (IOException e) {
            logger.error("read file fail ----->>>{}",e.getCause());
            return ;
        }finally {
            if (ftpClient.isConnected()){
                try {
                    ftpClient.disconnect();
                } catch (IOException e) {
                    logger.error("disconnect fail ------->>>{}",e.getCause());
                }
            }

            

        }

       




    }

}

1.9 - Spring专栏

所有和Spring相关的内容。

1.9.1 - 多个网卡时指定注册ip段.md

添加配置项

  cloud:
    inetutils:
      preferred-networks: 192.168.*

1.9.2 - 如何在pom文件中管理多个环境

dev dev true prod prod test test

1.9.3 - Spring 上传文件的配置

spring:

  servlet:
    multipart:
      #配置文件传输
      enabled: true
      file-size-threshold: 0B
      #单个文件的最大上限
      max-file-size: 100MB
      #单个请求的文件总大小上限
      max-request-size: 1000MB

1.9.4 - SpringBoot单元测试

测试均通过 SpringBoot3.5.0、JDK版本21。

简介

1. 完全脱离上下文(纯单元测试)

  • 实现方式Mockito + JUnit5(无Spring注解)

  • 特点

    • 仅测试类自身逻辑

    • 执行速度最快(毫秒级)

  • 适用场景:Service工具类、纯POJO逻辑测试

  • 代码示例


@ExtendWith(MockitoExtension.class)
class GoodsControllerTest { 
    private MockMvc mockMvc;

    // 需要模拟的控制器
    @InjectMocks // 可以注入要模拟的类
    private GoodsController goodsController;

    // 需要打桩的Service
    @Mock
    GoodsService goodsService;

    @BeforeEach
    public void initOne() {
        // 通过goodsController构建mvc模拟对象
        mockMvc = MockMvcBuilders.standaloneSetup(goodsController).build();
    }
}

2. 部分加载上下文(切片测试)

  • 实现方式@WebMvcTest/@DataJpaTest

  • 特点

    • 仅加载相关模块组件(如MVC层/JPA层)

    • 启动较快(秒级)

  • 适用场景:控制器测试、Repository接口测试

  • 代码示例


@WebMvcTest(HomeController.class) // HomeController要测试那个控制器
class GoodsControllerTest { 
    @Autowired
    private MockMvc mockMvc;

    // 需要打桩的Service
    @MockitoBean
    GoodsService goodsService;

}

3. 完全加载上下文(集成测试)

  • 实现方式@SpringBootTest

  • 特点

    • 加载完整Spring容器

    • 支持真实数据库交互

    • 启动慢(10秒+)

  • 适用场景:端到端测试、多组件协同测试

  • 代码示例


@AutoConfigureMockMvc
@SpringBootTest
class GoodsControllerTest { 
    @Autowired
    private MockMvc mockMvc;

    // 需要模拟的控制器
    @InjectMocks // 可以注入要模拟的类
    private GoodsController goodsController;

    // 需要打桩的Service
    @MockitoBean
    GoodsService goodsService;
}

测试用例

测试用例项目源代码: https://gitee.com/azhw/rookiedev-example/tree/master/编程/编程语言/Java/最佳实践/unittest/SpringBoot3UnitTest

  1. Service测试: no01servicetest

  2. Controller测试: no02ControllerTest

  3. Controller对Service打桩&Service对Mapper打桩的测试: no03MockitoService

  4. 对文件上传下载的测试: no04FileUploadDownload

  5. 使用H2数据库对数据写入进行实际测试: no05H2DBTest

在 no03MockitoService 其实已经包含了该测试内容

  1. 测试Controller 对 Cookie 、 Session 、 Header 的读写: no06CookieSessionHeaderTest

  2. 使用嵌入式redis服务器(embedded-redis)对Redis进行测试: no07RedisTest

这种场景只有使用完整的spring容器方式进行单元测试能进行测试, 其他两种(完全脱离spring和部分启动的方式只能对redisTemplate进行打桩测试)

  1. 测试SrpingBoot的Filter: no08SpringFilterTest

  2. 演示controller使用thymeleaf模板而不是返回json时的单元测试: no09ThymeleafTest

  3. 异步调用测试: no10AsyncTest

  4. Service超时测试: no11TimeoutTest

  5. 普通的类中(不是spring bean) 静态方法和非静态方法的打桩测试:no12NoSpringBeanMethodMockTest

1.10 - Stream API示例

import lombok.Data;
import org.junit.Test;

import java.util.*;
import java.util.stream.Collectors;

public class StreamAPI {

    /**
     *
     * Comparator.reverseOrder()是让某个条件进行倒序排序.
     * reversed()是让他前面的字段进行倒序。
     *
     * 例如:Comparator.comparing(名称).reversed(),此时名称倒序;
     * Comparator.comparing(名称).thenComparing(状态).reversed(),此时名称和状态进行倒序。
     *
     * Comparator.comparing(名称).thenComparing(状态).reversed().thenComparing(年龄),此时名称和状态会倒序,年龄会升序。
     *
     * 加多个.reversed()
     * Comparator.comparing(状态).reversed().thenComparing(年龄).reversed(),最终结果是只有年龄倒排,状态未排序

     */

    /**
     * 练习1:让年龄进行倒序排序。
     */
    @Test
    public void test1() {
        /*
         * Comparator.comparing(放实体类名称::放列名):添加排序字段;
         * reversed():倒序。
         * collect(Collectors.toList()):转成集合。
         * forEach(System.out::println):打印循环。
         */
        List<User> userList = generatorUserList();
//        userList.stream()
//                .sorted(Comparator.comparing(User::getAge).reversed())//reversed()是让其前面得所有字段都倒排
//                .collect(Collectors.toList())
//                .forEach(System.out::println);


//        List<User> userList2 = generatorUserList();
//        userList2.stream()
//                .sorted(Comparator.comparing(User::getAge, Comparator.reverseOrder()))//Comparator.reverseOrder()是让某个条件进行倒序排序
//                .collect(Collectors.toList()).forEach(System.out::println);

        userList.stream().sorted(Comparator.comparing(User::getAge).reversed())
                .collect(Collectors.toList()).forEach(System.out::println);
    }

    /**
     * 练习2:让使用状态进行升序排序。
     */
    @Test
    public void test2() {
        List<User> userList = generatorUserList();
        //默认是升序,所以只需要把排序字段放进去就行了。
        userList.stream().sorted(Comparator.comparing(User::getAge)).collect(Collectors.toList()).forEach(System.out::println);
    }

    /**
     * 练习3:让使用先状态和后年龄都进行倒序排序。
     */
    @Test
    public void test3() {
        List<User> userList = generatorUserList();
        userList.stream()
                //先根据使用状态倒序,而王五使用状态为2,所以王五排在第一。
                //而李四和张三使用状态都是1,就会触发另一个排序条件根据年龄排序
                .sorted(Comparator.comparing(User::getState).thenComparing(User::getAge).reversed())
                .collect(Collectors.toList())
                .forEach(System.out::println);

        //写法2
        List<User> userList2 = generatorUserList();
        userList2.stream()
                .sorted(
                        Comparator.comparing(User::getState, Comparator.reverseOrder())//根据state倒排
                        .thenComparing(User::getAge, Comparator.reverseOrder())//根据age倒排
                )
                .forEach(System.out::println);
    }

    /**
     * 练习4:让使用状态和年龄都进行升序排序。
     */
    @Test
    public void test4() {
        List<User> userList = generatorUserList();
        //默认就是升序
        userList.stream().sorted(Comparator.comparing(User::getState).thenComparing(User::getAge)).forEach(System.out::println);
    }

    /**
     * 练习5:让使用状态进行升序排序,年龄进行倒序排序,创建时间进行倒序排序。名字正排序
     * 注意:假如多个排序条件的排序方向不一致,需要倒序的字段应该用Comparator.reverseOrder(),每个倒排得字段单独写。
     */
    @Test
    public void test5() {
        List<User> userList = generatorUserList();
        userList.stream()
                .sorted(
                        Comparator.comparing(User::getState)
                                .thenComparing(User::getAge, Comparator.reverseOrder())
                                .thenComparing(User::getCreateTime,Comparator.reverseOrder())
                                .thenComparing(User::getName)
                )
                .forEach(System.out::println);
    }

    /**
     * 练习6,按 State 分组 组成 Map<Ingeter, List<User>>的结构
     */
    @Test
    public void test6() {
        List<User> userList = generatorUserList();
        Map<Integer, List<User>> stateToUserList = userList.stream().collect(Collectors.groupingBy(User::getState));
        System.out.println(stateToUserList);
    }

    /**
     * 联系7 根据姓名作为key, User对象为val,组成map<String, User>
     *     如果是根据state, 因为元素不唯一, 所以会抛出异常
     *     根据姓名为key, age为val组成 map<String, Ingeter>
     */
    @Test
    public void test7() {
        List<User> userList = generatorUserList();
        Map<String, User> nameToUser = userList.stream().collect(Collectors.toMap(User::getName, user -> user));
        //下面这样也行
//        Map<String, User> nameToUser = userList.stream().collect(Collectors.toMap(User::getName, user -> {return user;}));
        System.out.println(nameToUser);

        //在映射时, 把年龄加1再返回
        Map<String, User> nameToAge0 = userList.stream().collect(Collectors.toMap(User::getName, user -> { user.setAge(user.age+1); return user;}));
        System.out.println(nameToAge0);

        //那么如果分组的字段是一个非唯一的,比如state, 会报错
//        Map<Integer, User> stateToUser = userList.stream().collect(Collectors.toMap(User::getState, user -> user));
//        System.out.println(stateToUser);

        //根据姓名为key, age为val组成 map<String, Ingeter>
        //写法1
        Map<String, Integer> nameToAge = userList.stream().collect(Collectors.toMap(User::getName, User::getAge));
        System.out.println(nameToAge);
        //写法2
        Map<String, Integer> nameToAge2 = userList.stream().collect(Collectors.toMap(User::getName, user -> {return user.age;}));
        System.out.println(nameToAge2);

        Map<String, Integer> nameToAge3 = userList.stream().collect(Collectors.toMap(User::getName, user -> { user.setAge(user.age+1); return user.age;}));
        System.out.println(nameToAge3);

    }

    /**
     * 练习8 按名字去重
     */
    @Test
    public void test8() {
        List<User> userList = generatorUserList();
        Set<String> collect1 = userList.stream().distinct().map(User::getName).collect(Collectors.toSet());
        System.out.println(collect1);
        return;
    }

    /**
     * 按名字统计数量
     */
    @Test
    public void test9() {
        List<User> userList = generatorUserList();
        Map<String, Long> collect = userList.stream().collect(Collectors.groupingBy(User::getName, Collectors.counting()));
        System.out.println(collect);
        return;
    }

    /**
     * 求年龄最大值、最小值
     */
    @Test
    public void test10() {
        List<User> userList = generatorUserList();
        //如果有年龄一样的,会按list顺序取第一条, 所以除非能唯一, 否则多维度需要使用排序来获取值
        Optional<User> min = userList.stream().min(Comparator.comparing(User::getAge));
        System.out.println("min = " + min);
    }

    /**
     * 获取某个字段的 最大值、最小值、求和、统计、计数
     */
    @Test
    public void test11() {
        List<User> userList = generatorUserList();
        IntSummaryStatistics collect = userList.stream().collect(Collectors.summarizingInt(User::getAge));
        double average = collect.getAverage();
        int max = collect.getMax();
        int min = collect.getMin();
        long sum = collect.getSum();
        long count = collect.getCount();
        System.out.println("collect = " + collect);

        //单独对某个字段汇总
        int sum1 = userList.stream().mapToInt(User::getAge).sum();
        System.out.println("sum = " + sum1);

        double avg1 = userList.stream().collect(Collectors.averagingDouble(User::getAge));
        System.out.println("avg1 = " + avg1);

        OptionalDouble avg2 = userList.stream().mapToDouble(User::getAge).average();
        if (avg2.isPresent()) {
            System.out.println("avg2 = " + avg2);
        }
    }

    /**
     * 按名字分组, 统计年龄相关的 计数, 总数,最大,最小,平均
     */
    @Test
    public void test12() {
        List<User> userList = generatorUserList();
        Map<String, IntSummaryStatistics> collect = userList.stream().collect(Collectors.groupingBy(User::getName, Collectors.summarizingInt(User::getAge)));
        for(Map.Entry<String, IntSummaryStatistics> entry : collect.entrySet()) {
            System.out.println(entry.getKey()+"-----"+entry.getValue());
        }
    }

    /**
     * 生成用户示例用户列表
     * @return
     */
    private List<User> generatorUserList() {
        List<User> list = new ArrayList<>();

        User user1 = new User();
        user1.setName("19");
        user1.setAge(18);
        user1.setState(1);
        user1.setCreateTime(new Date());
        list.add(user1);

        User user2 = new User();
        user2.setName("1");
        user2.setAge(22);
        user2.setState(1);
        user2.setCreateTime(new Date());
        list.add(user2);

        User user3 = new User();
        user3.setName("2");
        user3.setAge(15);
        user3.setState(2);
        user3.setCreateTime(new Date());
        list.add(user3);

        User user4 = new User();
        user4.setName("23");
        user4.setAge(15);
        user4.setState(2);
        user4.setCreateTime(new Date());
        list.add(user4);

        User user5 = new User();
        user5.setName("23");
        user5.setAge(18);
        user5.setState(2);
        user5.setCreateTime(new Date());
        list.add(user5);
        return list;
    }







    @Data
    class User{
        private String name;

        private Integer age;

        private Integer state;

        private Date createTime;

    }
}

1.11 - String API



/**
 * public final class String
 * String 是包装类型,且为final修饰不能被继承
 * 一下提供字符串拼接和字符串反转的方法
 */
public class StringUtils {

    public static void main(String[] args) {
        StringUtils.stringJoin();
        StringUtils.stringReverse();
    }


    public static void stringJoin(){

        //StringBuffer字符串连接 线程安全的
        StringBuffer buffer = new StringBuffer();
        buffer.append("a");
        buffer.append("b");
        buffer.append("c");
        buffer.append("d");
        System.out.println(buffer.toString());

        //StringBuilder字符串连接, 非线程安全的,但是速度快
        StringBuilder builder = new StringBuilder();
        builder.append("a");
        builder.append("b");
        builder.append("c");
        builder.append("d");
        System.out.println(builder.toString());
    }


    //字符串反转
    public static void stringReverse(){
        // StringBuffer reverse
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("abcdefg");
        System.out.println(stringBuffer.reverse()); // gfedcba

        // StringBuilder reverse
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("abcdefg");
        System.out.println(stringBuilder.reverse()); // gfedcba
    }
}

1.12 - TryCache到底怎么返回值

package cn.anzhongwei.lean.demo;

public class TestTryCatch {
    public static void main(String[] args) {
        System.out.println(getInt1());
        System.out.println(getInt2());
    }

    public static int getInt1() {
        int a = 10;
        try {
            System.out.println(a / 0);
            a = 20;
        } catch (ArithmeticException e) {
            a = 30;
            return a;
            /*
             * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
             * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
             * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
             */
        } finally {
            a = 40;
        }
      return a;
    }

    public static int getInt2() {
        int a = 10;
        try {
            System.out.println(a / 0);
            a = 20;
        } catch (ArithmeticException e) {
            a = 30;
            return a;
            /*
             * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
             * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
             * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
             */
        } finally {
            a = 40;
            return a; //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
        }

//      return a;
    }
}

1.13 - Valid参数校验

简单使用

Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。

Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。接下来,我们以spring-boot项目为例,介绍Spring Validation的使用。

引入依赖

如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>

对于web服务来说,为防止非法参数对业务造成影响,在Controller层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  • POST、PUT请求,使用requestBody传递参数;

  • GET请求,使用requestParam/PathVariable传递参数。

下面我们简单介绍下requestBody和requestParam/PathVariable的参数校验实战!

requestBody参数校验

POST、PUT请求一般会使用requestBody传递参数,这种情况下,后端使用DTO对象进行接收。只要给DTO对象加上@Validated注解就能实现自动参数校验。比如,有一个保存User的接口,要求userName长度是2-10,account和password字段长度是6-20。

如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。

DTO表示数据传输对象(Data Transfer Object),用于服务器和客户端之间交互传输使用的。在spring-web项目中可以表示用于接收请求参数的Bean对象。

在DTO字段上声明约束注解

@Data
public class UserDTO {

    private Long userId;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}

在方法参数上声明校验注解

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

这种情况下,使用@Valid和@Validated都可以。

requestParam/PathVariable参数校验

GET请求一般会使用requestParam/PathVariable传参。如果参数比较多(比如超过6个),还是推荐使用DTO对象接收。

否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解(如@Min等)。如果校验失败,会抛出ConstraintViolationException异常。

代码示例如下:

@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    // 路径变量
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // 校验通过,才会执行业务逻辑处理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(userId);
        userDTO.setAccount("11111111111111111");
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }

    // 查询参数
    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
        // 校验通过,才会执行业务逻辑处理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(10000000000000003L);
        userDTO.setAccount(account);
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }
}

统一异常处理

前面说过,如果校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。

比如我们系统要求无论发送什么异常,http的状态码必须返回200,由业务码去区分系统的异常情况。

@RestControllerAdvice
public class CommonExceptionHandler {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
       return Result.fail(BusinessCode.参数校验失败, msg);
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.fail(BusinessCode.参数校验失败, ex.getMessage());
    }
}

进阶使用

分组校验

在实际项目中,可能多个方法需要使用同一个DTO类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在DTO类的字段上加约束注解无法解决这个问题。因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。

还是上面的例子,比如保存User的时候,UserId是可空的,但是更新User的时候,UserId的值必须>=10000000000000000L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:

约束注解上声明适用的分组信息groups

@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}

@Validated注解上指定校验分组

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

嵌套校验

前面的示例中,DTO类里面的字段都是基本数据类型和String类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。

比如,上面保存User信息的时候同时还带有Job信息。需要注意的是,此时DTO类的对应字段必须标记@Valid注解。

@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    @NotNull(groups = {Save.class, Update.class})
    @Valid
    private Job job;

    @Data
    public static class Job {

        @Min(value = 1, groups = Update.class)
        private Long jobId;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String jobName;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String position;
    }

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}

嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如List<Job>字段会对这个list里面的每一个Job对象都进行校验

集合校验

如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:

包装List类型,并声明@Valid注解

public class ValidationList<E> implements List<E> {

    @Delegate // @Delegate是lombok注解
    @Valid // 一定要加@Valid注解
    public List<E> list = new ArrayList<>();

    // 一定要记得重写toString方法
    @Override
    public String toString() {
        return list.toString();
    }
}

@Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校验不通过,会抛出NotReadablePropertyException,同样可以使用统一异常进行处理。

比如,我们需要一次性保存多个User对象,Controller层的方法可以这么写:

@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

自定义校验

业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。

自定义spring validation非常简单,假设我们自定义加密id(由数字或者a-f的字母组成,32-256长度)校验,主要分为两步:

自定义约束注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {

    // 默认错误消息
    String message() default "加密id格式错误";

    // 分组
    Class<?>[] groups() default {};

    // 负载
    Class<? extends Payload>[] payload() default {};
}

实现ConstraintValidator接口编写约束校验器

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 不为null才进行校验
        if (value != null) {
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

这样我们就可以使用@EncryptId进行参数校验了!

编程式校验

上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入javax.validation.Validator对象,然后再调用其api。

@Autowired
private javax.validation.Validator globalValidator;

// 编程式校验
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
    // 如果校验通过,validate为空;否则,validate包含未校验通过项
    if (validate.isEmpty()) {
        // 校验通过,才会执行业务逻辑处理

    } else {
        for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
            // 校验失败,做其它逻辑
            System.out.println(userDTOConstraintViolation);
        }
    }
    return Result.ok();
}

快速失败(Fail Fast)

Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 快速失败模式
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

@Valid和@Validated区别

图片

实现原理

requestBody参数校验实现原理

在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的参数以及处理@ResponseBody标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法resolveArgument()中:

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        parameter = parameter.nestedIfOptional();
        //将请求数据封装到DTO对象中
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                // 执行数据校验
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }
        return adaptArgumentIfNecessary(arg, parameter);
    }
}

可以看到,resolveArgument()调用了validateIfApplicable()进行参数校验。

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    // 获取参数注解,比如@RequestBody、@Valid、@Validated
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        // 先尝试获取@Validated注解
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        //如果直接标注了@Validated,那么直接开启校验。
        //如果没有,那么判断参数前是否有Valid起头的注解。
        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
            Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
            Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
            //执行校验
            binder.validate(validationHints);
            break;
        }
    }
}

看到这里,大家应该能明白为什么这种场景下@Validated、@Valid两个注解可以混用。我们接下来继续看WebDataBinder.validate()实现。

@Override
public void validate(Object target, Errors errors, Object... validationHints) {
    if (this.targetValidator != null) {
        processConstraintViolations(
            //此处调用Hibernate Validator执行真正的校验
            this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
    }
}

最终发现底层最终还是调用了Hibernate Validator进行真正的校验处理。

方法级别的参数校验实现原理

上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解的校验方式,就是方法级别的参数校验。

实际上,这种方式可用于任何Spring Bean的方法上,比如Controller/Service等。其底层实现原理就是AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强。

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        //为所有`@Validated`标注的Bean创建切面
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        //创建Advisor进行增强
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    //创建Advice,本质就是一个方法拦截器
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

接着看一下MethodValidationInterceptor:

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //无需增强的方法,直接跳过
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }
        //获取分组信息
        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            //方法入参校验,最终还是委托给Hibernate Validator来校验
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        //真正的方法调用
        Object returnValue = invocation.proceed();
        //对返回值做校验,最终还是委托给Hibernate Validator来校验
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}

实际上,不管是requestBody参数校验还是方法级别的校验,最终都是调用Hibernate Validator执行校验,Spring Validation只是做了一层封装。

各种检查注解示意

空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.  验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)

Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false

长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.

日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则 数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null

@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits 验证 Number 和 String 的构成是否合法 @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
@Range(min=, max=) 检查数字是否介于min和max之间.
@Range(min=10000,max=50000,message="range.bean.wage")
private BigDecimal wage;

@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)

最后再附上一个demo

  1. 被校验的类 User
import lombok.Data;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Data
public class User {

    @NotNull(message = "主键不能为空", groups = {CheckGroup1.class})
    private String id;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Size(min = 4, max = 16, message = "昵称长度为4~16个字符之间", groups = {CheckGroup1.class})
    private String nickName;

    @NotNull(message = "密码不能为空")
    @Size(min = 8, max = 24, message = "密码长度为8~24个字符之间")
    private String pwd;

}
  1. 校验结果实体ValidationResult
import lombok.Data;
import lombok.ToString;

import java.text.MessageFormat;
import java.util.Map;

/**
 * @Description 实体校验结果
 */
@Data
@ToString
public class ValidationResult {

    /**
     * 是否有异常
     */
    private boolean hasErrors;

    /**
     * 异常消息记录
     */
    private Map<String, String> errorMsg;

    /**
     * 获取异常消息组装
     *
     * @return
     */
    public String getMessage() {
        if (errorMsg == null || errorMsg.isEmpty()) {
            return "";
        }
        StringBuilder message = new StringBuilder();
        errorMsg.forEach((key, value) -> {
            message.append(MessageFormat.format("{0}:{1} \r\n", key, value));
        });
        return message.toString();
    }
}
  1. 校验Util类

import java.util.HashMap;
import java.util.Map;
import java.util.Set;


import org.apache.commons.collections4.CollectionUtils;
import org.hibernate.validator.HibernateValidator;

import javax.validation.*;
import javax.validation.groups.Default;

/**
 *
 */
public class ValidateUtils {

    private ValidateUtils() {
    }

    // 线程安全的,直接构建也可以,这里使用静态代码块一样的效果
    private static Validator validator;

    static {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    public static <T> void validate(T t) {
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);
        if (constraintViolations.size() > 0) {
            StringBuilder validateError = new StringBuilder();
            for (ConstraintViolation<T> constraintViolation : constraintViolations) {
                validateError.append(constraintViolation.getMessage()).append(";");
            }

            throw new ValidationException(validateError.toString());
        }
    }

    public static <T> void validate(T t, Class clazz) {
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(t, clazz);
        if (constraintViolations.size() > 0) {
            StringBuilder validateError = new StringBuilder();
            for (ConstraintViolation<T> constraintViolation : constraintViolations) {
                validateError.append(constraintViolation.getMessage()).append(";");
            }

            throw new ValidationException(validateError.toString());
        }
    }

    public static <T> void validate1(T t) {
        Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) // 为true,则有一个错误就结束校验
                .buildValidatorFactory().getValidator();

        Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);

        StringBuilder validateError = new StringBuilder();

        for (ConstraintViolation<T> constraintViolation : constraintViolations) {
            validateError.append(constraintViolation.getMessage()).append(";");
        }

        throw new ValidationException(validateError.toString());
    }

    // 自定义校验结果返回
    /**
     * 校验实体,返回实体所有属性的校验结果
     *
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> ValidationResult validateEntity(T obj) {
        // 解析校验结果
        Set<ConstraintViolation<T>> validateSet = validator.validate(obj, Default.class);
        return buildValidationResult(validateSet);
    }

    /**
     * 校验指定实体的指定属性是否存在异常
     *
     * @param obj
     * @param propertyName
     * @param <T>
     * @return
     */
    public static <T> ValidationResult validateProperty(T obj, String propertyName) {
        Set<ConstraintViolation<T>> validateSet = validator.validateProperty(obj, propertyName, Default.class);
        return buildValidationResult(validateSet);
    }

    /**
     * 将异常结果封装返回
     *
     * @param validateSet
     * @param <T>
     * @return
     */
    private static <T> ValidationResult buildValidationResult(Set<ConstraintViolation<T>> validateSet) {
        ValidationResult validationResult = new ValidationResult();
        if (!CollectionUtils.isEmpty(validateSet)) {
            validationResult.setHasErrors(true);
            Map<String, String> errorMsgMap = new HashMap<>();
            for (ConstraintViolation<T> constraintViolation : validateSet) {
                errorMsgMap.put(constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage());
            }
            validationResult.setErrorMsg(errorMsgMap);
        }
        return validationResult;
    }

    public static <T> ValidationResult validateEntity1(T obj) {
        ValidationResult result = new ValidationResult();
        Set<ConstraintViolation<T>> set = validator.validate(obj, Default.class);
        if (!CollectionUtils.isEmpty(set)) {
            result.setHasErrors(true);
            Map<String, String> errorMsg = new HashMap<String, String>();
            for (ConstraintViolation<T> cv : set) {
                errorMsg.put(cv.getPropertyPath().toString(), cv.getMessage());
            }
            result.setErrorMsg(errorMsg);
        }
        return result;
    }

    public static <T> ValidationResult validateEntity1(T obj, Class clazz) {
        ValidationResult result = new ValidationResult();
        Set<ConstraintViolation<T>> set = validator.validate(obj, clazz);
        if (!CollectionUtils.isEmpty(set)) {
            result.setHasErrors(true);
            Map<String, String> errorMsg = new HashMap<String, String>();
            for (ConstraintViolation<T> cv : set) {
                errorMsg.put(cv.getPropertyPath().toString(), cv.getMessage());
            }
            result.setErrorMsg(errorMsg);
        }
        return result;
    }

    public static <T> ValidationResult validateProperty1(T obj, String propertyName) {
        ValidationResult result = new ValidationResult();
        Set<ConstraintViolation<T>> set = validator.validateProperty(obj, propertyName, Default.class);
        if (!CollectionUtils.isEmpty(set)) {
            result.setHasErrors(true);
            Map<String, String> errorMsg = new HashMap<String, String>();
            for (ConstraintViolation<T> cv : set) {
                errorMsg.put(propertyName, cv.getMessage());
            }
            result.setErrorMsg(errorMsg);
        }
        return result;
    }

}
  1. 分组校验的交验组定义
public interface CheckGroup1 {
}
  1. 测试客户端
import java.util.Map;

public class Client {
    public static void main(String[] args) {
        User user = new User();
        ValidationResult validRes = ValidateUtils.validateEntity1(user, CheckGroup1.class);
        if (validRes.isHasErrors()) {
            for (Map.Entry<String, String> entry : validRes.getErrorMsg().entrySet()) {
                System.out.println(entry.getValue());
            }
        }
    }
}

1.14 - ZIPUtils

package cn.anzhongwei.lean.demo.zip;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ZipUtilFileInfo {
    private String name;
    private String url;

    private Boolean Local;
}
package cn.anzhongwei.lean.demo.zip;

import org.apache.commons.lang.ArrayUtils;
import org.apache.pdfbox.io.IOUtils;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.zip.*;

public class ZipUtils2 {
    private static final int BUFFER = 256;

    public static void main(String[] args) throws IOException {
            String hallFilePath = "d:/" +  "qwe";
        compress(Paths.get(hallFilePath).toString(), hallFilePath + ".zip");

        List<ZipUtilFileInfo> downFileInfos = new ArrayList<>();
        ZipUtilFileInfo d1 = new ZipUtilFileInfo( "价格导入模板.xlsx", "http://网络地址", false);
        ZipUtilFileInfo d2 = new ZipUtilFileInfo( "订单导入_仓储.xlsx", "http://网络地址", false);
        downFileInfos.add(d1);
        downFileInfos.add(d2);
        File zipfile1 = gerenatorZIPFileFromUrl(downFileInfos, "t1.zip");
        File zipfile2 = gerenatorZIPFileFromUrl(downFileInfos, "t2.zip");
        File zipfile3 = gerenatorZIPFileFromUrl(downFileInfos, "t3.zip");


        List<ZipUtilFileInfo> downFileInfos2 = new ArrayList<>();
        ZipUtilFileInfo dd1 = new ZipUtilFileInfo( zipfile1.getName(), zipfile1.getAbsolutePath(), true);
        ZipUtilFileInfo dd2 = new ZipUtilFileInfo( zipfile2.getName(), zipfile1.getAbsolutePath(), true);
        ZipUtilFileInfo dd3 = new ZipUtilFileInfo( zipfile3.getName(), zipfile1.getAbsolutePath(), true);
        downFileInfos2.add(dd1);
        downFileInfos2.add(dd2);
        downFileInfos2.add(dd3);

        File ff2 = gerenatorZIPFileFromUrl(downFileInfos2, "tt1.zip");

        System.out.println(zipfile1.getAbsolutePath());
        InputStream inputStream = new FileInputStream(ff2);
        File tempFile = new File("C:\\testFile\\");
        if (!tempFile.exists()) {
            tempFile.mkdirs();
        }
        OutputStream os = new FileOutputStream(tempFile.getPath() + File.separator + "shishi.zip");
        byte[] bs = new byte[1024];
        int len;
        while ((len = inputStream.read(bs)) != -1) {
            os.write(bs, 0, len);
        }
    }

    //文件或目录打包成zip
    public static void compress(String fromPath, String toPath) throws IOException {
        File fromFile = new File(fromPath);
        File toFile = new File(toPath);
        if (!fromFile.exists()) {
            throw new RuntimeException(fromPath + "不存在!");
        }
        try (FileOutputStream outputStream = new FileOutputStream(toFile);
             CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new CRC32());
             ZipOutputStream zipOutputStream = new ZipOutputStream(checkedOutputStream)) {
            String baseDir = "";
            compress(fromFile, zipOutputStream, baseDir);
        }
    }



    private static void compress(File file, ZipOutputStream zipOut, String baseDir) throws IOException {
        if (file.isDirectory()) {
            compressDirectory(file, zipOut, baseDir);
        } else {
            compressFile(file, zipOut, baseDir);
        }
    }


    private static void compressFile(File file, ZipOutputStream zipOut, String baseDir) throws IOException {
        if (!file.exists()) {
            return;
        }
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
            ZipEntry entry = new ZipEntry(baseDir + file.getName());
            zipOut.putNextEntry(entry);
            int count;
            byte[] data = new byte[BUFFER];
            while ((count = bis.read(data, 0, BUFFER)) != -1) {
                zipOut.write(data, 0, count);
            }
        }
    }



    private static void compressDirectory(File dir, ZipOutputStream zipOut, String baseDir) throws IOException {
        File[] files = dir.listFiles();
        if (files != null && ArrayUtils.isNotEmpty(files)) {
            for (File file : files) {
                compress(file, zipOut, baseDir + dir.getName() + File.separator);
            }
        }
    }

    private static File gerenatorZIPFileFromUrl(List<ZipUtilFileInfo> downFileInfos, String zipFileName) {

        try {
            File zipFile = File.createTempFile(zipFileName, ".zip");
            FileOutputStream f = new FileOutputStream(zipFile);
            CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());
            ZipOutputStream zos = new ZipOutputStream(csum);
            for (ZipUtilFileInfo downFileInfo : downFileInfos) {
                InputStream inputStream;
                if (Objects.isNull(downFileInfo.getLocal())) {
                    throw new RuntimeException("url类型未指定是否本地,请检查代码");
                }
                if (downFileInfo.getLocal()) {
                    inputStream = new FileInputStream(downFileInfo.getUrl());
                } else {
                    inputStream = getStreamByUrl(downFileInfo.getUrl());
                }
                zos.putNextEntry(new ZipEntry(downFileInfo.getName()));
                int bytesRead = 0;
                // 向压缩文件中输出数据
                while((bytesRead = inputStream.read()) != -1){
                    zos.write(bytesRead);
                }
                inputStream.close();
                zos.closeEntry();
            }
            zos.close();
            return zipFile;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    public static InputStream getStreamByUrl(String strUrl){
        HttpURLConnection conn = null;
        try {
            URL url = new URL(strUrl);
            conn = (HttpURLConnection)url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(20 * 1000);
            final ByteArrayOutputStream output = new ByteArrayOutputStream();
            IOUtils.copy(conn.getInputStream(),output);
            return  new ByteArrayInputStream(output.toByteArray());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try{
                if (conn != null) {
                    conn.disconnect();
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }

}

1.15 - 遍历List

package cn.anzhongwei.lean.demo.list.arraylist;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        Object obj = new Object();
        List<Object> list = new ArrayList<>();

        int objCount = 1000;
        for (int i = 0; i < objCount; i++) {
            list.add(obj);
        }

        int testTimes = 5;
        for (int i = 0; i < testTimes; i++) {
            testFor(list);
            testForEnhanced(list);
            testForEach(list);
            testIterator(list);
            System.out.println("\n");
        }
        System.out.println("---------------------------------------------------------\n");

        List<Object> list2 = new ArrayList<>();
        for (int i = 0; i < objCount; i++) {
            list2.add(obj);
        }

        for (int i = 0; i < testTimes; i++) {
            testFor(list2);
            testForEnhanced(list2);
            testForEach(list2);
            testIterator(list2);
            System.out.println();
        }
    }

    private static void testFor(List<Object> list) {
        long startTime = 0L;
        long endTime = 0L;
        startTime = System.nanoTime();

        for (int i = 0; i < list.size(); i++) {
            Object o = list.get(i);
        }
        endTime = System.nanoTime();
        System.out.println("for所用时间(ns)      :" + (endTime - startTime));
    }

    private static void testForEnhanced(List<Object> list) {
        long startTime = 0L;
        long endTime = 0L;
        startTime = System.nanoTime();

        for (Object o : list) {
            Object value = o;
        }

        endTime = System.nanoTime();
        System.out.println("增强for所用时间(ns)  :" + (endTime - startTime));
    }

    private static void testForEach(List<Object> list) {
        long startTime = 0L;
        long endTime = 0L;
        startTime = System.nanoTime();

        list.forEach(o->{Object obj = o;});

        endTime = System.nanoTime();
        System.out.println("forEach所用时间(ns)  :" + (endTime - startTime));
    }

    private static void testIterator(List<Object> list) {
        long startTime = 0L;
        long endTime = 0L;
        startTime = System.nanoTime();

        Iterator<Object> iterator = list.iterator();
        while (iterator.hasNext()) {
            Object o = iterator.next();
        }
        endTime = System.nanoTime();
        System.out.println("iterator所用时间(ns) :" + (endTime - startTime));
    }
}

1.16 - 遍历Map

package cn.anzhongwei.lean.demo.map;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class MapTest {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("1", "value1");
        map.put("2", "value2");
        map.put("3", "value3");

        //第一种:普遍使用,二次取值
        System.out.println("通过Map.keySet遍历key和value:");
        for (String key : map.keySet()) {
            System.out.println("key= "+ key + " and value= " + map.get(key));
        }

        //第二种
        System.out.println("通过Map.entrySet使用iterator遍历key和value:");
        Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
        }

        //第三种:推荐,尤其是容量大时
        System.out.println("通过Map.entrySet遍历key和value");
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
        }

        //第四种
        System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
        for (String v : map.values()) {
            System.out.println("value= " + v);
        }
    }
}

1.17 - 从mybatis的sql日志自动填充sql占位符

应该由bug,没详细测试
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 从mybatis的sql日志自动填充sql占位符
 */
public class SqlReplaceUtil extends JFrame {

    private static final Pattern HUMP_PATTERN = Pattern.compile("\\?");
    JPanel jp;
    JButton b1;
    JTextArea sqlTextField;
    JTextArea paramTextField;
    JTextArea resultTextField;
    JLabel lb3;
    JLabel lb4;

    SqlReplaceUtil() {
        jp = new JPanel();
        b1 = new JButton("替换");
        sqlTextField = new JTextArea("输入待处理的SQL,比如:insert into test (id,name,age) values (?,?,?)",10,90);
        paramTextField = new JTextArea("输入待处理参数,比如:100(Integer),zhangsan(String),null",10,90);
        resultTextField = new JTextArea(10,90);
        lb3 = new JLabel("结果为:");
        lb4 = new JLabel("");
        b1.addActionListener(new ActionListener() {//响应按钮的事件
            public void actionPerformed(ActionEvent e) {
                try {
                    lb4.setText("");
                    String sql = sqlTextField.getText();
                    String param = paramTextField.getText();
                    resultTextField.setText(replaceSql(sql, param));
                } catch (Exception ex) {
                    lb4.setText(ex.getMessage());
                }
            }

        });
        jp.add(sqlTextField);
        jp.add(paramTextField);
        jp.add(b1);
        jp.add(resultTextField);
        jp.add(lb4);

        add(jp);
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setLocation(600, 200);
        setVisible(true);
        pack();
        setSize(1050, 700);
        setLocationRelativeTo(null);
        setVisible(true);
        setResizable(false);
    }

    public static void main(String[] args) {
        SqlReplaceUtil e = new SqlReplaceUtil();
        Container contentPane = e.getContentPane();
        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));
    }

    private String replaceSql(String sql, String param) {
        String[] split = param.split(",");
        //校验数量
        int paramCount = split.length;
        int matcherCount = 0;
        Matcher matcher = HUMP_PATTERN.matcher(sql);
        while (matcher.find()) {
            matcherCount++;
        }
        if (paramCount != matcherCount) {
            throw new RuntimeException("待替换参数和参数数量不一致");
        }
        //处理参数
        for (int i = 0; i < split.length; i++) {
            split[i] = split[i].trim();
            int index = split[i].lastIndexOf('(');
            if (split[i].equals("null")||index<0) {
                continue;
            }
            split[i] = "'" + split[i].substring(0, index) + "'";
        }
        StringBuffer sb = new StringBuffer();
        Matcher matcher2 = HUMP_PATTERN.matcher(sql);
        int index = 0;
        while (matcher2.find()) {
            matcher2.appendReplacement(sb, split[index]);
            index++;
        }
        matcher2.appendTail(sb);
        return sb.toString();
    }
}

1.18 - 代理模式实现

1.18.1 - ByteBuddy动态代理

java17以后替换cglib方式的动态代理。他和cglib实现动态代理的优势是不用定义接口也不需要去实现接口,对现有的代码扩展更友好。
  • pom文件添加依赖
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <!-- 我用的最新的, 一般1.14.0以上都没问题 -->
            <version>1.15.1</version>
        </dependency>
  1. 先创建要被代理的类
public class TargetClass {
    public void performAction() {
        System.out.println("原始方法: performAction");
    }

    public String sayHello(String name) {
        return "Hello, " + name;
    }
}
  1. 创建拦截器
import net.bytebuddy.implementation.bind.annotation.*;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

/**
 * 通用方法拦截器
 */
public class GenericInterceptor {

    @RuntimeType
    public static Object intercept(@Origin Method method,
                                   @AllArguments Object[] args,
                                   @This Object obj,
                                   @SuperCall Callable<?> callable) throws Exception {
        // 获取动态添加的字段值
        String proxyField = (String) obj.getClass().getField("proxyField").get(obj);
        System.out.println("[拦截] proxyField 值: " + proxyField);

        // 修改字段值
        System.out.println("[通用拦截] 修改字段proxyField值: Modified by interceptor" );
        obj.getClass().getField("proxyField").set(obj, "Modified by interceptor");

        System.out.println("[通用拦截] 方法开始: " + method.getName());
        Object result = callable.call(); // 执行原始方法
        System.out.println("[通用拦截] 方法结束: " + method.getName());
        return result;
    }
}
  1. 代理工具
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 通用 Byte Buddy 动态代理工具类
 */
public class ByteBuddyProxy {

    /**
     * 创建一个代理对象
     *
     * @param targetClass 目标类类型
     * @param interceptor 拦截器类(需使用 Byte Buddy 注解定义逻辑)
     * @param <T>         泛型
     * @return 代理实例
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @throws InstantiationException
     */
    public static <T> T createProxy(Class<T> targetClass, Class<?> interceptor) throws Exception {

        Class<?> dynamicType = new ByteBuddy()
                .subclass(targetClass)
                // ElementMatchers.any() 拦截所有方法
                .method(ElementMatchers.any())
                //  MethodDelegation.to(interceptor) 拦截器类 方法增强
                .intercept(MethodDelegation.to(interceptor))
                // 动态的添加字段。这里可以放到参数里面,然后由调用方传递进来要添加什么参数
                .defineField("proxyField", String.class, Visibility.PUBLIC) // 可选:添加字段
                .defineField("abc", String.class, Visibility.PUBLIC) // 可选:添加字段
                .make()
                .load(targetClass.getClassLoader())
                .getLoaded();
                // 支持构造实例
//                .getDeclaredConstructor()
//                .newInstance();

        Constructor<?> constructor = dynamicType.getDeclaredConstructor();
        Object instance = constructor.newInstance();

        // 现在就可以 调用方法触发拦截逻辑 了
        // ((TargetClass) instance).performAction();
        // 设置字段值
        System.out.println("[通用拦截] 设置字段proxyField值: Hello Dynamic Field" );
        dynamicType.getField("proxyField").set(instance, "Hello Dynamic Field");

        System.out.println("[通用拦截] 设置字段abc值: def" );
        dynamicType.getField("abc").set(instance, "def");
        return (T) instance;

    }
}
  1. 一个测试类用来测试动态代理
public class ByteBuddyGenericExample {
    public static void main(String[] args) throws Exception {
        // 创建代理对象
        TargetClass proxyInstance = ByteBuddyProxy
                .createProxy(TargetClass.class, GenericInterceptor.class);

        // 调用方法
        proxyInstance.performAction();
        String result = proxyInstance.sayHello("World");
        System.out.println("返回值: " + result);

        String abc = (String) proxyInstance.getClass().getField("abc").get(proxyInstance);
        System.out.println("last在打印一遍proxyField: " + abc);
    }
}

1.18.2 - CGLIB动态代理

CGLIB基于ASM实现。提供比反射更为强大的动态特性。使用CGLIB可以非常方便的实现的动态代理。但是由于jdk17+由于java.base模块未向未命名模块开放java.lang包,所以CGLIB无法在jdk17+中运行。在java17+后 需要使用 Byte Buddy,这里cglib就没法测试了,

maven项目需要添加依赖

        <!-- https://mvnrepository.com/artifact/cglib/cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>

1 0.1 CGLIB包结构

  • *net.sf.cglib.core    *底层字节码处理类。

  • *net.sf.cglib.transform    *该包中的类用于class文件运行时转换或编译时转换。

  • *net.sf.cglib.proxy    *该包中的类用于创建代理和方法拦截。

  • *net.sf.cglib.reflect    *该包中的类用于快速反射,并提供了C#风格的委托。

  • *net.sf.cglib.util    *集合排序工具类。

  • *net.sf.cglib.beans    *JavaBean工具类。

1 使用CGLIB实现动态代理

2 1.1 CGLIB代理相关的类

  • net.sf.cglib.proxy.Enhancer    主要的增强类。
  • net.sf.cglib.proxy.MethodInterceptor    主要的方法拦截类,它是Callback接口的子接口,需要用户实现。
  • net.sf.cglib.proxy.MethodProxy    JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用。

cglib是通过动态的生成一个子类去覆盖所要代理类的非final方法,并设置好callback,则原有类的每个方法调用就会转变成调用用户定义的拦截方法(interceptors)。

CGLIB代理相关的常用API如下图所示:

net.sf.cglib.proxy.Callback接口在CGLIB包中是一个重要的接口,所有被net.sf.cglib.proxy.Enhancer类调用的回调(callback)接口都要继承这个接口。

net.sf.cglib.proxy.MethodInterceptor能够满足任何的拦截(interception )需要。对有些情况下可能过度。为了简化和提高性能,CGLIB包提供了一些专门的回调(callback)类型:

  • net.sf.cglib.proxy.FixedValue 为提高性能,FixedValue回调对强制某一特别方法返回固定值是有用的。
  • net.sf.cglib.proxy.NoOp NoOp回调把对方法调用直接委派到这个方法在父类中的实现。
  • net.sf.cglib.proxy.LazyLoader 当实际的对象需要延迟装载时,可以使用LazyLoader回调。一旦实际对象被装载,它将被每一个调用代理对象的方法使用。
  • net.sf.cglib.proxy.Dispatcher Dispathcer回调和LazyLoader回调有相同的特点,不同的是,当代理方法被调用时,装载对象的方法也总要被调用。
  • net.sf.cglib.proxy.ProxyRefDispatcher ProxyRefDispatcher回调和Dispatcher一样,不同的是,它可以把代理对象作为装载对象方法的一个参数传递。

3 1.2 CGLIB动态代理的基本原理

CGLIB动态代理的原理就是用Enhancer生成一个原有类的子类,并且设置好callback到proxy, 则原有类的每个方法调用都会转为调用实现了MethodInterceptor接口的proxy的intercept() 函数,如图

在intercept()函数里,除执行代理类的原因方法,在原有方法前后加入其他需要实现的过程,改变原有方法的参数值,即可以实现对原有类的代理了。这似于AOP中的around advice。

4 1.3 使用MethodInterceptor接口实现方法回调

当对代理中所有方法的调用时,都会转向MethodInterceptor类型的拦截(intercept)方法,在拦截方法中再调用底层对象相应的方法。下面我们举个例子,假设你想对目标对象的所有方法调用进行权限的检查,如果没有经过授权,就抛出一个运行时的异常。

net.sf.cglib.proxy.MethodInterceptor接口是最通用的回调(callback)类型,它经常被基于代理的AOP用 来实现拦截(intercept)方法的调用。

MethodInterceptor接口只定义了一个方法:

public Object intercept(Object object, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable;

参数Object object是被代理对象,不会出现死循环的问题。

参数java.lang.reflect.Method method是java.lang.reflect.Method类型的被拦截方法。

参数Object[] args是被被拦截方法的参数。

参数MethodProxy proxy是CGLIB提供的MethodProxy 类型的被拦截方法。

注意:

1、若原方法的参数存在基本类型,则对于第三个参数Object[] args会被转化成类的类型。如原方法的存在一个参数为int,则在intercept方法中,对应的会存在一个Integer类型的参数。

2、若原方法为final方法,则MethodInterceptor接口无法拦截该方法。

4.1 1.3.1 实现MethodInterceptor接口

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

class MethodInterceptorImpl implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before invoke " + method);
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After invoke" + method);
        return result;
    }
}

Object result=proxy.invokeSuper(o,args); 表示调用原始类的被拦截到的方法。这个方法的前后添加需要的过程。在这个方法中,我们可以在调用原方法之前或之后注入自己的代码。

由于性能的原因,对原始方法的调用使用CGLIB的net.sf.cglib.proxy.MethodProxy对象,而不是反射中一般使用java.lang.reflect.Method对象。

5 1.4 使用CGLIB代理最核心类Enhancer生成代理对象

net.sf.cglib.proxy.Enhancer中有几个常用的方法:

  • void setSuperclass(java.lang.Class superclass) 设置产生的代理对象的父类。
  • void setCallback(Callback callback) 设置CallBack接口的实例。
  • void setCallbacks(Callback[] callbacks) 设置多个CallBack接口的实例。
  • void setCallbackFilter(CallbackFilter filter) 设置方法回调过滤器。
  • Object create() 使用默认无参数的构造函数创建目标对象。
  • Object create(Class[], Object[]) 使用有参数的构造函数创建目标对象。参数Class[] 定义了参数的类型,第二个Object[]是参数的值。

注意:在参数中,基本类型应被转化成类的类型。

基本代码:

public Object createProxy(Class targetClass) {

    Enhancer enhancer = new Enhancer();

    enhancer.setSuperclass(targetClass);

    enhancer.setCallback(new MethodInterceptorImpl ());

    return enhancer.create();

}

createProxy方法返回值是targetClass的一个实例的代理。

6 1.5 使用CGLIB继进行动态代理示例

例1:使用CGLIB生成代理的基本使用。

import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class TestMain {

public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Cglib.class);
        enhancer.setCallback(new HelloProxy());
        Cglib cglibProxy = (Cglib)enhancer.create();
        cglibProxy.cglib();
    }
}

class Cglib{
    public void cglib(){
        System.out.println("CGLIB");
    }
}

class HelloProxy implements MethodInterceptor{
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Hello");
        Object object = proxy.invokeSuper(obj, args);
        System.out.println("Powerful!");
        return object;
    }
}

输出内容:

Hello

CGLIB

Powerful!

例2:使用CGLIB创建一个Dao工厂,并展示一些基本特性。

public interface Dao {
    void add(Object o);
    void add(int i);
    void add(String s);
}

public class DaoImpl implements Dao {

    @Override
    public void add(Object o) {
        System.out.println("add(Object o)");
    }

    @Override
    public void add(int i) {
        System.out.println("add(int i)");
    }

    public final void add(String s) {
        System.out.println("add(String s)");
    }
}

    public class Proxy implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        System.out.println("拦截前...");
        // 输出参数类型
        for (Object arg : args) {
            System.out.print(arg.getClass() + ";");
        }
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("拦截后...");
        return result;
    }
}

public class DaoFactory {

    public static Dao create() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(DaoImpl.class);
        enhancer.setCallback(new Proxy());
        Dao dao = (Dao) enhancer.create();
        return dao;
    }
}

public class TestMain {

    public static void main(String[] args) {
        Dao dao = DaoFactory.create();
        dao.add(new Object());
        dao.add(1);
        dao.add("1");
    }
}

输出内容:

拦截前...

class java.lang.Object;add(Object o)

拦截后...

拦截前...

class java.lang.Integer;add(int i)

拦截后...

add(String s)

2 回调过滤器CallbackFilter

net.sf.cglib.proxy.CallbackFilter有选择的对一些方法使用回调。

CallbackFilter可以实现不同的方法使用不同的回调方法。所以CallbackFilter称为"回调选择器"更合适一些。

CallbackFilter中的accept方法,根据不同的method返回不同的值i,这个值是在callbacks中callback对象的序号,就是调用了callbacks[i]。


import java.lang.reflect.Method;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.NoOp;

public class CallbackFilterDemo {

    public static void main(String[] args) {
        // 回调实例数组
        Callback[] callbacks = new Callback[]{new MethodInterceptorImpl(), NoOp.INSTANCE};
        // 使用enhancer,设置相关参数。
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(User.class);
        enhancer.setCallbacks(callbacks);
        enhancer.setCallbackFilter(new CallbackFilterImpl());

        // 产生代理对象
        User proxyUser = (User) enhancer.create();
        proxyUser.pay(); // 买
        proxyUser.eat(); // 吃
    }
}
/**
 * 回调过滤器类。
 */
public class CallbackFilterImpl implements CallbackFilter {

    @Override
    public int accept(Method method) {
        String methodName = method.getName();
        if ("eat".equals(methodName)) {
            return 1; // eat()方法使用callbacks\[1\]对象拦截。
        } else if ("pay".equals(methodName)) {
            return 0; // pay()方法使用callbacks\[0\]对象拦截。
        }
        return 0;
    }
}

/**
 * 自定义回调类。
 */
public class MethodInterceptorImpl implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before invoke " + method);
        Object result = proxy.invokeSuper(obj, args); // 原方法调用。
        System.out.println("After invoke" + method);
        return result;
    }
}
    

class User {
    public void pay() {
        System.out.println("买东西");
    }

    public void eat() {
        System.out.println("吃东西");
    }
}

输出结果:

Before invoke public void sjq.cglib.filter.User.pay()

pay()

After invokepublic void sjq.cglib.filter.User.pay()

eat()

3 CGLIB对Mixin的支持

CGLIB的代理包net.sf.cglib.proxy.Mixin类提供对Minix编程的支持。Minix允许多个对象绑定到一个单个的大对象上。在代理中对方法的调用委托到下面相应的对象中。 这是一种将多个接口混合在一起的方式 , 实现了多个接口。

Minix是一种多继承的替代方案, 很大程度上解决了多继承的很多问题 , 实现和理解起来都比较容易。

import net.sf.cglib.proxy.Mixin;

public class MixinDemo {

    public static void main(String[] args) {

        //接口数组
        Class<?>[] interfaces = new Class[] { MyInterfaceA.class, MyInterfaceB.class };
        //实例对象数组
        Object[] delegates = new Object[] { new MyInterfaceAImpl(), new MyInterfaceBImpl() };
        //Minix组合为o对象。
        Object o = Mixin.create(interfaces, delegates);
        MyInterfaceA a = (MyInterfaceA) o;
        a.methodA();
        MyInterfaceB b = (MyInterfaceB) o;
        b.methodB();
        System.out.println("\\r\\n 输出Mixin对象的结构...");
        Class clazz = o.getClass();
        Method[] methods = clazz.getDeclaredMethods();
        for (int i = 0; i < methods.length; i++) {
            System.out.println(methods[i].getName());
        }
        System.out.println(clazz);
    }
}

interface MyInterfaceA {
    public void methodA();
}

interface MyInterfaceB {
    public void methodB();
}

class MyInterfaceAImpl implements MyInterfaceA {
    @Override
    public void methodA() {
        System.out.println("MyInterfaceAImpl.methodA()");
    }
}

class MyInterfaceBImpl implements MyInterfaceB {
    @Override
    public void methodB() {
        System.out.println("MyInterfaceBImpl.methodB()");
    }
}

输出结果:

MyInterfaceAImpl.methodA()

MyInterfaceBImpl.methodB()

输出Mixin对象的结构...

methodA

methodB

newInstance

class sjq.cglib.mixin.MyInterfaceA

M ixin ByC GLI B MixinByCGLIB

d1f6261a

4 CGLIB用来对象之间拷贝属性

package sjq.cglib.bean.copy;

import net.sf.cglib.beans.BeanCopier;

public class PropertyCopyDemo {

    public static void main(String[] args) {
    
        //两个对象
        Other other = new Other("test", "1234");
        Myth myth = new Myth();
        System.out.println(other);
        System.out.println(myth);
        //构建BeanCopier,并copy对象的属性值。
        BeanCopier copier = BeanCopier.create(Other.class, Myth.class, false);
        copier.copy(other, myth, null);
        System.out.println(other);
        System.out.println(myth);
    }
}

class Other {

    private String username;
    
    private String password;
    
    private int age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Other(String username, String password) {
        super();
        this.username = username;
        this.password = password;
    }
    
    @Override
    public String toString() {
        return "Other: " + username + ", " + password + ", " + age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

class Myth {

    private String username;
    
    private String password;
    
    private String remark;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
    @Override
    public String toString() {
        return "Myth: " + username + ", " + password + ", " + remark;
    }
    public void setRemark(String remark) {
        this.remark = remark;
    }
    public String getRemark() {
        return remark;
    }
}

运行结果如下:

Other: test, 1234, 0

Myth: null, null, null

Other: test, 1234, 0

Myth: test, 1234, null

5 使用CGLIB动态生成Bean

import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;

/**
* 动态实体bean
*/
public class CglibBean {

    /**
    * 实体Object
    */
    public Object object = null;
    /**
    * 属性map
    */
    public BeanMap beanMap = null;
    
    public CglibBean() {
        super();
    }

    @SuppressWarnings("unchecked")
    public CglibBean(Map<String, Class> propertyMap) {
        this.object = generateBean(propertyMap);
        this.beanMap = BeanMap.create(this.object);
    }

    /**
    * 给bean属性赋值
    * @param property属性名
    * @param value值
    */
    public void setValue(String property, Object value) {
        beanMap.put(property, value);
    }

    /**
    * 通过属性名得到属性值
    * @param property属性名
    */
    public Object getValue(String property) {
        return beanMap.get(property);
    }

    /**
    * 得到该实体bean对象。
    */
    public Object getObject() {
        return this.object;
        }

    /**
    * 生成Bean
    * @param propertyMap
    * @return
    */
    @SuppressWarnings("unchecked")
    private Object generateBean(Map<String, Class> propertyMap) {
        BeanGenerator generator = new BeanGenerator();
        Set keySet = propertyMap.keySet();
        for (Iterator i = keySet.iterator(); i.hasNext();) {
            String key = (String) i.next();
            generator.addProperty(key, (Class) propertyMap.get(key));
        }
        return generator.create();
    }
}

测试并使用动态Bean

import java.lang.reflect.Method;
import java.util.HashMap;

/**
 * Cglib测试类
*/
public class CglibTest {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws ClassNotFoundException {

    // 设置类成员属性
    HashMap<String, Class> propertyMap = new HashMap<String, Class>();
    propertyMap.put("id", Class.forName("java.lang.Integer"));
    propertyMap.put("name", Class.forName("java.lang.String"));
    propertyMap.put("address", Class.forName("java.lang.String"));

    // 生成动态Bean
    CglibBean bean = new CglibBean(propertyMap);

    // 给Bean设置值
    bean.setValue("id", new Integer(123));
    bean.setValue("name", "454");
    bean.setValue("address", "789");

    // 从Bean中获取值,当然了获得值的类型是Object
    System.out.println(">>id=" + bean.getValue("id"));
    System.out.println(">>name=" + bean.getValue("name"));
    System.out.println(">>address=" + bean.getValue("address"));// 获得bean的实体
    Object object = bean.getObject();

    // 通过反射查看所有方法名
    Class clazz = object.getClass();
    Method[] methods = clazz.getDeclaredMethods();
    for (int i = 0; i < methods.length; i++) {
            System.out.println(methods[i].getName());
        }
    }
}

输出:

\>>id=123

\>>name=454

\>>address=789

setId

getAddress

getName

getId

setName

setAddress

class net.sf.cglib.empty.Object

B eanG ener ator ByC GLI B BeanGeneratorByCGLIB

1d39cfaa

1.18.3 - 动态代理

动态代理实现 MethodInterceptor 他可以在使用时再确定要代理什么并且由使用者决定要调用的方法。本质上是使用了反射来实现的
  1. 定义一个接口
public interface IRegisterService {
    void register(String name, String pwd);
}
  1. 一个接口的实现类
public class RegisterServiceImpl implements IRegisterService {
    @Override
    public void register(String name, String pwd) {
        System.out.println(String.format("【向数据库中插入数据】name:%s,pwd:%s", name, pwd));
    }
}
  1. 一个实现了InvocationHandler接口的类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

class InsertDataHandler implements InvocationHandler {

    // 持有一个被代理类的引用, 但是这个被代理类不是具体的类。而是超类
    Object obj;

    public Object getProxy(Object obj){
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        doBefore();
        // 在这里调用被代理的方法
        Object result = method.invoke(obj, args);
        doAfter();
        return result;
    }
    private void doBefore() {
        System.out.println("[Proxy]一些前置处理");
    }
    private void doAfter() {
        System.out.println("[Proxy]一些后置处理");
    }

}
  1. 测试
public class DynamicProxy {
    public static void main(String[] args) {
        // 实例化被代理类
        IRegisterService iRegisterService = new RegisterServiceImpl();

        // 实例化代理类
        InsertDataHandler insertDataHandler = new InsertDataHandler();

        // 通过Proxy获取代理类
        IRegisterService proxy = (IRegisterService)insertDataHandler.getProxy(iRegisterService);

        // 执行方法
        proxy.register("RyanLee", "123");
    }
}

1.18.4 - 静态代理

静态代理的代理类持有要代理的目标类的引用。代理关系是确定的。每一个需要代理的类都需要一个代理类来持有他的引用
  1. 定义一个接口 IRegisterService
public interface IRegisterService {
    void register(String name, String pwd);
}
  1. 创建被代理的类,并实现 接口
public class RegisterServiceImpl implements IRegisterService {
    @Override
    public void register(String name, String pwd) {
        System.out.println(String.format("【向数据库中插入数据】name:%s,pwd:%s", name, pwd));
    }
}
  1. 创建代理类,并实现接口
class RegisterServiceProxy implements IRegisterService {
    // 代理类持有被代理类对象的引用
    IRegisterService iRegisterService;

    public RegisterServiceProxy(IRegisterService iRegisterService) {
        this.iRegisterService = iRegisterService;
    }

    @Override
    public void register(String name, String pwd) {
        System.out.println("[Proxy]一些前置处理");
        //这里调用被代理类的方法,也是静态指定的
        iRegisterService.register(name, pwd);
        System.out.println("[Proxy]一些后置处理");

    }
}
  1. 测试
public class StaticProxy {
    public static void main(String[] args) {
        IRegisterService iRegisterService = new RegisterServiceImpl();
        IRegisterService proxy = new RegisterServiceProxy(iRegisterService);
        proxy.register("RyanLee", "123");
    }
}

1.19 - 定义一个由builder来构造的实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * description: Java Builder模式练习
 * @version v1.0
 * @author w
 * @date 2021年7月7日上午11:37:49
 **/
@AllArgsConstructor
@Data
@NoArgsConstructor
public class User {
    private String id ;
    private String name ;
    private Integer age ;

    public static Builder builder(){
        return new Builder();
    }

    public static class Builder{
        private String id ;
        private String name ;
        private Integer age ;

        public Builder id(String id) {
            this.id = id ;
            return this;
        }

        public Builder name(String name) {
            this.name = name ;
            return this ;
        }

        public Builder age(Integer age) {
            this.age = age ;
            return this;
        }

        public User build() {
            return new User(this);
        }

        public User build2() {
            return new User(this.id , this.name , this.age);
        }

    }

    public User(Builder builder) {
        this.id = builder.id;
        this.name = builder.name ;
        this.age = builder.age;
    }

}
public class TestBuilder {
    public static void main(String[] args) {
        User user = User.builder().id("1").name("xiaoming").age(18).build();
        System.out.println(user);
    }
}

1.20 - 多线程

多线程

1.20.1 - 方法一: 继承Thread


import java.io.IOException;

/**
 * 方法一: 继承Thread
 */
public class M01ExtentThread {

    public static void main(String[] args) throws IOException {
        M01ExtentThread test = new M01ExtentThread();
        MyThread thread1 = test.new MyThread();
        MyThread thread2 = test.new MyThread();
        thread1.setName("线程1");
        thread2.setName("线程2");
        thread1.start();
        thread2.start();
        System.out.println("主线程输出");
    }

    class MyThread extends Thread{
        @Override
        public void run() {
            for(int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName()+"线程输出"+i);
            }
        }
    }
}

1.20.2 - 方法二实现Runnable接口

/**
 * 方法2实现Runnable接口
 */
public class M02ImplentRunnable {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 3; i++) {
                    System.out.println("runnablex线程输出"+i);
                }
            }
        };
        new Thread(runnable).start();
        System.out.println("主线程输出");
    }
}

1.20.3 - 方法三实现Callable接口

package cn.anzhongwei.lean.demo.thread;

import java.util.Random;
import java.util.concurrent.*;

/**
 * 和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
 *
 * call()方法可以有返回值
 *
 * call()方法可以声明抛出异常
 *
 * Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口。
 * 因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
 * public interface Future<V> {
 *     //视图取消该Future里面关联的Callable任务
 *     boolean cancel(boolean mayInterruptIfRunning);
 *     //如果在Callable任务正常完成前被取消,返回True
 *     boolean isCancelled();
 *     //若Callable任务完成,返回True
 *     boolean isDone();
 *     //返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
 *     V get() throws InterruptedException, ExecutionException;
 *     //返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
 *     V get(long timeout, TimeUnit unit)
 *         throws InterruptedException, ExecutionException, TimeoutException;
 * }
 *
 * 介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:
 *
 * 1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
 *
 * 2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
 *
 * 3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
 *
 * 4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
 */
public class M03ImplementCallable01 {

    public static void main(String[] args) {
        //1. 此处使用的是匿名类方式
//        Callable<String> callable = new Callable<String>() {
//            public String call() throws Exception {
//                for(int i = 0; i < 3; i++) {
//                    System.out.println("callable线程输出"+i);
//                }
//                return "返回随机数"+(new Random().nextInt(100));
//            }
//        };
        //2. 使用静态内部类
        Callable<String> callable = new InnerStaticCallable();
        //3. 使用外部实现类。略
        FutureTask<String> future = new FutureTask<String>(callable);
        Thread thread = new Thread(future);
        thread.start();
        try {
            String resI = future.get();
            System.out.println(resI);
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        } catch (ExecutionException ee) {
            ee.printStackTrace();
        }

        //此种方法可以用, 但是不明确应用场景, 返回资源的值是在new FutureTask中传入的,
        //runnable接口实现直接使用Thread的start方法运行就行,传到FutureTask中有啥用
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 3; i++) {
                    System.out.println("Runnable线程输出"+i);
                }
            }
        };
        FutureTask<Integer> target = new FutureTask<>(runnable, 12);
        Thread thread1 = new Thread(target);
        thread1.start();
        try {
            Integer resI1 = target.get();
            System.out.println(resI1);
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        } catch (ExecutionException ee) {
            ee.printStackTrace();
        }
        System.out.println("主线程输出");

    }
    //也可以使用内部类方式
    static class InnerStaticCallable implements Callable<String> {
        @Override
        public String call() {
            for(int i = 0; i < 3; i++) {
                System.out.println("callable线程输出"+i);
            }
            return "返回随机数"+(new Random().nextInt(100));
        }
    }
}

1.20.4 - 方法四Executors框架


import java.util.concurrent.*;

/**
 * 1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。
 * 要执行任务的人只需把Task描述清楚,然后提交即可。
 * 这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。
 * 具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。
 * Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。
 * 因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,
 * 还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。
 *
 * 关于ExecutorService执行submit和execute的区别
 * 1、接收的参数不一样
 * 2、submit有返回值,而execute没有
 * 3、submit方便Exception处理
 *    如果你在你的task里会抛出checked或者unchecked exception,而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。
 */
public class M04ExccutorCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService1 = Executors.newSingleThreadExecutor();
        ExecutorService executorService2 = Executors.newSingleThreadExecutor();


        Runnable runnable = new Runnable(){
            @Override
            public void run() {
                for(int i = 0; i < 3; i++) {
                    System.out.println("Runnable接口"+i);
                }
            }
        };
        //执行Runnable接口,无返回值
        executorService1.execute(runnable);
        executorService1.shutdown();


        //执行Callable接口,有返回值
        Callable<String> stringCallable = new Callable(){
            @Override
            public String call() throws Exception {
                for(int i = 0; i < 3; i++) {
                    System.out.println("Callable接口"+i);
                }
                return "Hello World";
            }
        };
        Future<String> submit2 = executorService2.submit(stringCallable);
        executorService2.shutdown();
        //get操作会造成执行线程的阻塞
        String submit2Res = submit2.get();
        System.out.println(submit2Res);

        System.out.println("main Thread output");
    }
}

1.20.5 - 线程池


import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class TestThreadPoolExecutor {
    /*
    corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
    当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
    如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
    maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
    如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
    值得注意的是,如果使用了无界的任务队列这个参数就没用了

    执行:
    1、2任务直接压入1、2线程直接创建并执行,
    3任务放入缓冲队列,最大线程数未满,创建新线程执行3任务

     */
    public static void main(String[] args) throws InterruptedException, IOException {
        int corePoolSize = 2;
        int maximumPoolSize = 10;
        long keepAliveTime = 10;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
        ThreadFactory threadFactory = new NameTreadFactory();
        RejectedExecutionHandler handler = new MyIgnorePolicy();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit,
                workQueue, threadFactory);
        executor.prestartAllCoreThreads(); // 预启动所有核心线程

        for (int i = 1; i <= 10; i++) {
//            Thread.sleep(1);
            MyTask task = new MyTask(String.valueOf(i));
            executor.execute(task);
            System.out.println("当前队列数两"+executor.getQueue().size());
        }
        System.out.println(executor.getActiveCount());
        Thread.sleep(10000);
        System.out.println(executor.getActiveCount());
        System.in.read(); //阻塞主线程
    }

    static class NameTreadFactory implements ThreadFactory {

        private final AtomicInteger mThreadNum = new AtomicInteger(1);

        @Override
        public Thread newThread(Runnable r) {
//            try {
//                Thread.sleep(200);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            Thread t = new Thread(r, "my-thread-" + mThreadNum.getAndIncrement());
            System.out.println(t.getName() + " has been created");
            return t;
        }
    }

    public static class MyIgnorePolicy implements RejectedExecutionHandler {

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            doLog(r, e);
        }

        private void doLog(Runnable r, ThreadPoolExecutor e) {
            // 可做日志记录等
            System.err.println( r.toString() + " rejected");
//          System.out.println("completedTaskCount: " + e.getCompletedTaskCount());
        }
    }

    static class MyTask implements Runnable {
        private String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                System.out.println(this.toString() + " is running!");
                Thread.sleep(1000); //让任务执行慢点
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "MyTask [name=" + name + "]";
        }
    }
}

1.20.6 - 线程交替执行的实践

线程交替执行的实践 也就是 1a2b3c4d交替输出的问题
  1. 只能保证交替执行, 至于先输出1 还是先输出A 谁先抢到o锁谁就输出
public class Syncwaitnotify1 {

    public static void main(String[] args) {
        final Object o = new Object();
        char[] aI = "123456789".toCharArray();
        char[] aC = "ABCDEFGHI".toCharArray();
        new Thread(()->{
            //1. 先拿到o锁
            synchronized (o) {
                for(char c : aI) {
                    //2. 执行
                    System.out.print(c);
                    try {
                        o.notify();//唤醒线程队列中得下一个线程, 这里只有两个,就相当于唤醒另一个
                        o.wait();//让出锁,重新进入等待队列
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }
        }, "t1").start();

        new Thread(()->{
            //1. 和上面得Thread强锁
            synchronized (o) {
                for(char i : aC) {
                    System.out.print(i);
                    try {
                        o.notify();
                        o.wait();
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }

        }, "t2").start();
    }
}
  1. 先等待一直等到CountDownLatch计数器为0, 所以这个程序一定先输出A

import java.util.concurrent.CountDownLatch;

public class Syncwaitnotify2 {
    /*
    CountDownLatch latch = new CountDownLatch(n);
    latch.countDown();每执行一次,n-1
     */
    private static CountDownLatch latch = new CountDownLatch(1);

    public static void main(String[] args) {
        final Object o = new Object();
        char[] aI = "123456789".toCharArray();
        char[] aC = "ABCDEFGHI".toCharArray();
        new Thread(()->{
            try{
                latch.await();
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            //1. 先拿到o锁
            synchronized (o) {
                for(char c : aI) {
                    //2. 执行
                    System.out.print(c);
                    try {
                        o.notify();//唤醒线程队列中得下一个线程, 这里只有两个,就相当于唤醒另一个
                        o.wait();//让出锁,重新进入等待队列
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }
        }, "t1").start();

        new Thread(()->{
            //1. 和上面得Thread强锁
            synchronized (o) {
                for(char i : aC) {
                    System.out.print(i);
                    latch.countDown();
                    try {
                        o.notify();
                        o.wait();
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }

        }, "t2").start();


    }
}
  1. 通过volatile 实现 volatile是原子操作并且线程可见,也可以保证顺序执行

/**
 * 如果t1先拿到锁,此时t25 = false 在while中不断得会让出锁
 * 等t2拿到锁后, 设置t25=true就可以正常交替执行了
 */
public class Syncwaitnotify3 {

    private static volatile boolean t25 = false;

    public static void main(String[] args) {
        final Object o = new Object();
        char[] aI = "123456789".toCharArray();
        char[] aC = "ABCDEFGHI".toCharArray();
        new Thread(()->{
            //1. 如果t1先拿到锁
            synchronized (o) {
                //2. 此时t25=false
                while (!t25) {
                    try {
                        //3. 让出线程, 进入等待队列
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                for(char c : aI) {
                    System.out.print(c);
                    try {
                        o.notify();//唤醒线程队列中得下一个线程, 这里只有两个,就相当于唤醒另一个
                        o.wait();//让出锁,重新进入等待队列
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }
        }, "t1").start();

        new Thread(()->{
            //4. 此时t2后拿到锁
            synchronized (o) {
                for(char i : aC) {
                    System.out.print(i);
                    t25 = true;
                    try {
                        o.notify();
                        o.wait();
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }

        }, "t2").start();


    }
}
  1. 指定线程锁,相互唤醒,第一次也使用latch.await(); 保证, 和2差不多

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockCondition {
    public static void main(String[] args) {
        char[] aI = "123456789".toCharArray();
        char[] aC = "ABCDEFGHI".toCharArray();

        Lock lock = new ReentrantLock();
        Condition conditionT1 = lock.newCondition();
        Condition conditionT2 = lock.newCondition();

        CountDownLatch latch = new CountDownLatch(1);

        new Thread(()->{
            try {
                //1. 启动线程直接进入等待
                latch.await();
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            try {
                for(char c : aI) {
                    System.out.print(c);
                    conditionT2.signal();
                    conditionT1.await();
                }
                conditionT2.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();

        new Thread(()->{
            lock.lock();
            try {
                //2. 线程启动直接进入循环进行输出
                for(char c : aC) {
                    System.out.print(c);
                    //3. 标记完成
                    latch.countDown();
                    //4. 唤醒conditionT1队列
                    conditionT1.signal();
                    conditionT2.await();
                }
                conditionT1.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}
  1. LockSupport的实现

import java.util.concurrent.locks.LockSupport;

public class LockSupport1 {
    static Thread t1=null, t2=null;
    public static void main(String[] args) {
        char[] aI = "123456789".toCharArray();
        char[] aC = "ABCDEFGHI".toCharArray();

        t1 = new Thread(() -> {
            for(char c: aI) {
                LockSupport.park();
                System.out.print(c);
                LockSupport.unpark(t2);
            }
        },"t1");

        t2 = new Thread(() -> {
            for(char c: aC) {


                System.out.print(c);
                LockSupport.unpark(t1);
                LockSupport.park();

            }
        },"t2");

        t1.start();
        t2.start();
    }
}
  1. TransferQueue的实现

import java.util.concurrent.LinkedTransferQueue;

public class TransferQueue1 {
    public static void main(String[] args) {
        char[] aI = "123456789".toCharArray();
        char[] aC = "ABCDEFGHI".toCharArray();
        java.util.concurrent.TransferQueue<Character> queue = new LinkedTransferQueue<>();

        new Thread(() ->{
            try{
                for(char c: aI) {
                    System.out.print(queue.take());
                    queue.transfer(c);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(() ->{
            try{
                for(char c: aC) {
                    queue.transfer(c);
                    System.out.print(queue.take());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

1.20.7 - Callable捕获异常


import java.util.concurrent.*;

/**
 * Callable捕获异常
 */
public class CallableGetThrow {
    public static void main(String[] args){
        int timeout = 2;
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Boolean result = false;

        Future<Boolean> future = executor.submit(new TaskThread("发送请求"));//将任务提交给线程池

        try {
            System.out.println("try");
            result = future.get(timeout, TimeUnit.SECONDS);//获取结果
            future.cancel(true);
            System.out.println("发送请求任务的返回结果:"+result);  //2
        } catch (InterruptedException e) {
            System.out.println("线程中断出错。"+e);
            future.cancel(true);// 中断执行此任务的线程
        } catch (ExecutionException e) {
            System.out.println("线程服务出错。");
            future.cancel(true);
        } catch (TimeoutException e) {// 超时异常
            System.out.println("超时。");
            future.cancel(true);
        }finally{
            System.out.println("线程服务关闭。");
            executor.shutdown();
        }
    }

    static class TaskThread implements Callable<Boolean> {
        private String t;
        public TaskThread(String temp){
            this.t= temp;
        }

        /*
        try
        继续执行..........
        发送请求任务的返回结果: true
        线程服务关闭。
         */
//        public Boolean call() throws InterruptedException {
//            Thread.currentThread().sleep(1000);
//            System.out.println("继续执行..........");
//            return true;
//        }

        /*
        try
        超时。
        线程服务关闭。
         */
//        public Boolean call() throws InterruptedException {
//            Thread.currentThread().sleep(3000);
//            System.out.println("继续执行..........");
//            return true;
//        }

        /*
         * try
         * start
         * 线程服务出错。
         * 线程服务关闭。
         */
        public Boolean call() throws InterruptedException {
            System.out.println("start");
            throw new InterruptedException();
        }

    }
}

1.21 - 根据对象层次路径动态获取和设置值

先看一段json

{
    "wmPlans":
    [
        {
            "skuInfos":
            [
                {
                    "customerItemNO":"no-03",
                    "goCargoId":"go03"
                },
                {
                    "customerItemNO":"no-04",
                    "goCargoId":"go04",
                    "extensionParameter":
                    {
                        "invNo":"abcd01",
                        "bu":"BM"
                    }

                }

            ],
            "warehouseType":"wmsHouse1",
            "warehousedocumentType":"wmsDocument1"
        },
        {
            "skuInfos":
            [
                {
                    "customerItemNO":"no-03",
                    "goCargoId":"go03"
                },
                {
                    "customerItemNO":"no-04",
                    "goCargoId":"go04"
                }

            ],
            "warehouseType":"wmsHouse2",
            "warehousedocumentType":"wmsDocument2"
        }

    ],
    "goOrder":
    {
        "extionParam":
        {
            "bbb":"ccc"
        },
        "skuInfos":
        [
            {
                "customerItemNO":"no-01",
                "goCargoId":"go01"
            },
            {
                "customerItemNO":"no-02",
                "goCargoId":"go02"
            }

        ],
        "i3":20,
        "mark3":"mark3",
        "consignorCode":"ConsignorCode01"
    }

}

我希望能根据一个路径

例如

goOrder.skuInfos.customerItemNO 获取到两个值 no-01 no-02

goOrder.skuInfos[0].customerItemNO 获取到 no-01

wmPlans.skuInfos.customerItemNO 获取到 no-03 no-04 no-03 no-04

wmPlans[0].skuInfos.customerItemNO 获取到 no-03 no-04

wmPlans.skuInfos[0].customerItemNO 获取到 no-03 no-03

不仅仅是获取, 我还希望可以对这个深层次的对象进行赋值 所以有了这个小例子

先定义数据结构

  1. OrderInfoDTO
import lombok.Data;
import lombok.ToString;

import java.util.List;

@Data
@ToString
public class OrderInfoDTO {
    private GoOrderDTO goOrder;
    private List<WmsPlanDTO> wmPlans;
}
  1. GoOrderDTO
import lombok.Data;
import lombok.ToString;

import java.util.List;
import java.util.Map;

@Data
@ToString
public class GoOrderDTO {
    private String consignorCode;
    private String mark3;
    private Integer i3;
    private List<SkuInfoDTO> skuInfos;
    private Map<String, String> extionParam;
}
  1. WmsPlanDTO
import lombok.Data;
import lombok.ToString;

import java.util.List;

@Data
@ToString
public class WmsPlanDTO {
    private String warehouseType;
    private String warehousedocumentType;
    private List<SkuInfoDTO> skuInfos;
}
  1. SkuInfoDTO
import lombok.Data;
import lombok.ToString;

import java.util.Map;

@Data
@ToString
public class SkuInfoDTO {
    private String customerItemNO;
    private String goCargoId;
    private Map<String, String> extensionParameter;
}

定义反射方法这是我们的核心

  1. ReflectGetPathValRes
import lombok.Data;

@Data
public class ReflectGetPathValRes {
    private Boolean success;
    private String val;

    public ReflectGetPathValRes(Boolean success, String val) {
        this.success = success;
        this.val = val;
    }
}
  1. ReflectUtil 这个类方法里面很多重复代码,可以考虑封装成函数

import cn.hutool.json.JSONUtil;
import org.apache.commons.lang.StringUtils;
import java.lang.reflect.Field;
import java.util.*;

public class ReflectUtil {

    /**
     * 通过反射,对rootObj 对象  对应conditionPath路径 赋值为val
     * @param rootObj
     * @param conditionPath
     * @param val
     * @return
     */
    public static boolean reflectAssignEvaluate(Object rootObj, String conditionPath, Object val) {
        if (conditionPath.isEmpty()) {
            System.out.println("条件比较路径发生空值错误错误");
            return false;
        }
        String[] paths = StringUtils.split(conditionPath, '.');
        int pathsLen = paths.length;
        Object currObj = rootObj;
        Object[] objArr = new Object[pathsLen+1];
        objArr[0] = rootObj;
        for(int i=0; i <= pathsLen-1; i++) {
            try{
                Class currObjClass = currObj.getClass();
                String currGetDeclaredPath = paths[i];
                // 虽然命名是mapKey 但是他也会获取到list的索引值
                String mapKey = "";
                if (StringUtils.contains(currGetDeclaredPath, '[')) {
                    currGetDeclaredPath = paths[i].substring(0, paths[i].indexOf('['));
                    mapKey = paths[i].substring(paths[i].indexOf('[')+1, paths[i].indexOf(']'));
                }

                Field currObjField = currObjClass.getDeclaredField(currGetDeclaredPath);
                currObjField.setAccessible(true);
                currObj = currObjField.get(currObj);
                if(currObj != null) {
                    objArr[i+1] = currObj;
                }
                //如果获取到当前对象为空,并且路径不是最后一级,则为错误
                if (currObj == null && i < pathsLen-1) {
                    System.out.println(currObjField.getType().getTypeName()+"."+currObjField.getName()+"为空,下一级属性无法获取及赋值");
                    return false;
                }
                //赋值操作
                if(i == pathsLen-1) {
                    return objAssignEvaluate(currObjField, objArr[i], val, mapKey);
                }
                if (currObjField.getType().getTypeName().contains("List")) {
                    List srcList=(List)currObj;
                    String newPaths = StringUtils.join(arraySub(paths, i+1, pathsLen),'.');
                    if (StringUtils.isNotBlank(mapKey) && StringUtils.isNumeric(mapKey)) {
                        int mapKeyIndex = Integer.parseInt(mapKey);
                        srcList.get(mapKeyIndex);
                        return reflectAssignEvaluate(srcList.get(mapKeyIndex), newPaths, val);

                    } else {
                        boolean assignEvaluateRes = true;
                        for(Object src : srcList) {

                            if (!reflectAssignEvaluate(src, newPaths, val)) {
                                assignEvaluateRes = false;
                            }
                        }
                        return assignEvaluateRes;
                    }


                }
            }catch (NoSuchFieldException | IllegalAccessException e) {
                System.out.println(e.getMessage());
                return false;
            }
        }
        return true;
    }

    /**
     * 利用反射,校验条件路径下对象中的值是否与期望值相同
     * 期望值都是String类型,因为是从数据库中获取到的,所以无法判定具体类型
     * @param rootObj
     * @param conditionPath
     * @param expVal
     * @return
     */
    public static boolean reflectCheckCondition(Object rootObj, String conditionPath, String expVal) {
        ReflectGetPathValRes getValRes = reflectGetPathVal(rootObj, conditionPath);
        if (getValRes.getSuccess()) {
            if (Objects.isNull(getValRes.getVal())) {
                getValRes.setVal("");
            }
            return StringUtils.equals(getValRes.getVal(), expVal);
        }
        return false;
    }

    /**
     * 通过反射,对rootObj 对象  对应conditionPath路径 代表得值
     * 本方法支持map对应得key值
     * list获取对应索引后得内容
     * @param rootObj
     * @param conditionPath
     * @return
     */
    public static ReflectGetPathValRes reflectGetPathVal(Object rootObj, String conditionPath) {
        if (conditionPath.isEmpty()) {
            System.out.println("条件比较路径发生空值错误错误");
            return new ReflectGetPathValRes(false,"");
        }
        String[] paths = StringUtils.split(conditionPath, '.');
        int pathsLen = paths.length;
        Object currObj = rootObj;
        Object[] objArr = new Object[pathsLen+1];
        objArr[0] = rootObj;
        for(int i=0; i <= pathsLen-1; i++) {
            try{
                Class currObjClass = currObj.getClass();
                String currGetDeclaredPath = paths[i];
                String mapKey = "";
                if (StringUtils.contains(currGetDeclaredPath, '[')) {
                    currGetDeclaredPath = paths[i].substring(0, paths[i].indexOf('['));
                    mapKey = paths[i].substring(paths[i].indexOf('[')+1, paths[i].indexOf(']'));
                }

                Field currObjField = currObjClass.getDeclaredField(currGetDeclaredPath);
                currObjField.setAccessible(true);
                currObj = currObjField.get(currObj);
                if(currObj != null) {
                    objArr[i+1] = currObj;
                }
                //如果获取到当前对象为空,并且路径不是最后一级,则为错误
                if (currObj == null && i < pathsLen-1) {
                    System.out.println(currObjField.getType().getTypeName()+"."+currObjField.getName()+"为空,下一级属性无法获取及赋值");
                    return new ReflectGetPathValRes(false,"");
                }
                //赋值操作
                if(i == pathsLen-1) {
                    return new ReflectGetPathValRes(true,fromObjGetFieldValue(currObjField, objArr[i], mapKey));
                }
                if (currObjField.getType().getTypeName().contains("List")) {
                    List srcList=(List)currObj;
                    String newPaths = StringUtils.join(arraySub(paths, i+1, pathsLen),'.');
                    if (StringUtils.isNotBlank(mapKey)) {
                        return reflectGetPathVal(srcList.get(Integer.parseInt(mapKey)),newPaths);
                    }
                    List<Object> resList = new ArrayList<>();
                    for(Object src : srcList) {
//                        return reflectGetPathVal(src,newPaths);
                        ReflectGetPathValRes getPathValRes = reflectGetPathVal(src,newPaths);
                        // 检查 getPathValRes.getVal() 是否是一个jsonarray
                        if (getPathValRes.getVal().startsWith("[")) {
                            resList.addAll(JSONUtil.toList(JSONUtil.parseArray(getPathValRes.getVal()), Object.class));
                        } else {
                            resList.add(getPathValRes.getVal());
                        }

                    }

                    return new ReflectGetPathValRes(true, JSONUtil.toJsonStr(resList));
                }
            }catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
                return new ReflectGetPathValRes(false,"");
            }
        }
        return new ReflectGetPathValRes(false,"");
    }


    public static boolean objAssignEvaluate(Field field, Object rootObj, Object val, String mapKey) throws IllegalAccessException {
        if (field.getType().getName().contains("Integer")) {
            field.set(rootObj, Integer.parseInt(String.valueOf(val)));return true;
        }else if (field.getType().getName().contains("String")) {
            field.set(rootObj, String.valueOf(val));
        } else if (field.getType().getName().contains("Map")) {
            Map<String, String> currMap = (Map<String, String>) field.get(rootObj);
            if (currMap == null) {
                currMap = new HashMap<>();
            }
            currMap.put(mapKey, String.valueOf(val));
            field.set(rootObj, currMap);
        } else if (field.getType().getName().contains("List")) {
            List srcList=(List)field.get(rootObj);
            if (srcList == null) {
                srcList = new ArrayList();
            }
            if (StringUtils.isNotBlank(mapKey) && StringUtils.isNumeric(mapKey)) {
                int index = Integer.valueOf(mapKey);
                srcList.set(index, val);
//                field.set(rootObj, srcList);
            } else {
                field.set(rootObj, val);
            }

        }
        //其他类型的强制转换
        return true;
    }

    /**
     * 从rootObj中获取字段field得值
     * @param field
     * @param rootObj
     * @param mapKey
     * @return
     * @throws IllegalAccessException
     */
    public static String fromObjGetFieldValue(Field field, Object rootObj, String mapKey) throws IllegalAccessException {
        if (field.getType().getName().contains("Integer")) {
            Integer realVal = (Integer)field.get(rootObj);
            return String.valueOf(realVal);
        } else if (field.getType().getName().contains("String")) {
            return (String)field.get(rootObj);
        } else if (field.getType().getName().contains("Map")) {
            Map<String, String> currMap = (Map<String, String>) field.get(rootObj);
            return currMap.get(mapKey);
        } else if (field.getType().getName().contains("List")) {
            List srcList=(List)field.get(rootObj);
            // mapkey不是空并且他还是一个数字
            if (StringUtils.isNotBlank(mapKey) && StringUtils.isNumeric(mapKey)) {

                int index = Integer.valueOf(mapKey);
                return JSONUtil.toJsonStr(srcList.get(index));
            }
            return JSONUtil.toJsonStr(srcList);
        }
        //其他类型的强制转换
        return "";
    }

    public static String[] arraySub(String[] data, int start, int end) {
        String[] c = new String[end-start];
        int j = 0;
        for(int i=start; i<end; i++){
            c[j] = data[i];
            j++;
        }
        return c;
    }

    //对对象中所有字符串类型的空对象赋值空字符串
    public static void javaBeanNullStrToEmptyStr(Object obj){
        Class currObjClass = obj.getClass();
        Field[] fields = currObjClass.getDeclaredFields();
        for(Field field : fields) {
            field.setAccessible(true);
            if (field.getType().getName().contains("String")) {
                try {
                    String currValue = (String) field.get(obj);
                    if (StringUtils.isBlank(currValue)) {
                        objAssignEvaluate(field, obj, "", "");
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    System.out.println(e.toString());
                }
            }

        }
    }
}

接下来我们进行测试

TestMain.java

import cn.hutool.json.JSONUtil;
import java.util.*;

public class TestMain {
    public static void main(String[] args) {
        OrderInfoDTO orderInfoDTO = initData();

        System.out.println(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(orderInfoDTO)));


/********************************************************************************/
          // 第一次测试 一个正常的属性路径
//        String path1 = "goOrder.consignorCode";
//        String expValTrue="ConsignorCode01";
//        // 检查一个正确的值
//        boolean res11 = ReflectUtil.reflectCheckCondition(orderInfoDTO, path1, expValTrue);//true
//        System.out.println(res11); // true
//
//        // 重新赋值
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path1, "ConsignorCode03");
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path1)); // ConsignorCode03
/********************************************************************************/

        // 第二次测试 一个arraylist的属性路径
//        String path2 = "goOrder.skuInfos";
//        String expValTrue="[{\"customerItemNO\":\"no-01\",\"goCargoId\":\"go01\"},{\"customerItemNO\":\"no-02\",\"goCargoId\":\"go02\"}]";
//        boolean resTrue = ReflectUtil.reflectCheckCondition(orderInfoDTO, path2, expValTrue);
//        System.out.println(resTrue);
//        List<SkuInfoDTO> skuList = new ArrayList<>();
//        SkuInfoDTO skuInfoDTO1 = new SkuInfoDTO();
//        skuInfoDTO1.setGoCargoId("goSku01");
//        skuInfoDTO1.setCustomerItemNO("noSKU01");
//
//        SkuInfoDTO skuInfoDTO2 = new SkuInfoDTO();
//        skuInfoDTO2.setGoCargoId("goSku02");
//        skuInfoDTO2.setCustomerItemNO("noSku02");
//        skuList.add(skuInfoDTO1);
//        skuList.add(skuInfoDTO2);
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path2, skuList);
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path2));

/********************************************************************************/

          // 第三次测试对数组中的某个元素的属性路径
//        String path3 = "goOrder.skuInfos[0].goCargoId";
//        String expValTrue="go01";
//
//        boolean resTrue = ReflectUtil.reflectCheckCondition(orderInfoDTO, path3, expValTrue);
//        System.out.println(resTrue); // true
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path3, "goCargo01");
//        System.out.println(JSONUtil.toJsonStr(orderInfoDTO)); // goCargo01

/********************************************************************************/

        // 第四次测试 数组的指定索引进行赋值和取值
//        String path4 = "goOrder.skuInfos[0]";
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path4));
//        String expValTrue="{\"customerItemNO\":\"no-01\",\"goCargoId\":\"go01\"}";
//        boolean resTrue = ReflectUtil.reflectCheckCondition(orderInfoDTO, path4, expValTrue);
//
//        SkuInfoDTO skuInfoDTO1 = new SkuInfoDTO();
//        skuInfoDTO1.setGoCargoId("goSku01");
//        skuInfoDTO1.setCustomerItemNO("noSKU01");
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path4, skuInfoDTO1);
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path4));

/********************************************************************************/

        // 第五次测试 此次测试的赋值无意义,几乎不会有吧所有数组元素赋值成完全一样内容的需求
//        String path5 = "wmPlans.skuInfos";
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path5));
        // 这样取值会将上层数组平铺开然后展示出来 [{"customerItemNO":"no-01","goCargoId":"go01"},{"customerItemNO":"no-02","goCargoId":"go02"},{"customerItemNO":"no-03","goCargoId":"go03"},{"customerItemNO":"no-04","goCargoId":"go04","extensionParameter":{"bbb":"ccc"}}]

/********************************************************************************/

        // 第六次测试 第一层是数组,第二层指定索引
//        String path6 = "wmPlans.skuInfos[0].customerItemNO";
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path6));
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path6, "shishi");
//        System.out.println(JSONUtil.toJsonStr(orderInfoDTO));

/********************************************************************************/

        // 第七次测试 第一层是数组指定索引,第二层是数组
//        String path7 = "wmPlans[0].skuInfos.customerItemNO";
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path7));
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path7, "shishi");
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path7));
//        System.out.println(JSONUtil.toJsonStr(orderInfoDTO));

/********************************************************************************/

        // 第八次测试 第一层是数组,然后跟一个普通的属性
//        String path8 = "wmPlans.warehouseType";
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path8));
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path8, "warehouseTypeTh8");
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path8));

/********************************************************************************/


//        String path9 = "goOrder.skuInfos.goCargoId";
//        System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path9));
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO,path9, "123456");
//        System.out.println(JSONUtil.toJsonStr(orderInfoDTO));

/********************************************************************************/

        // 第十次测试 给 所有的wmPlans 里面所有的sku 添加扩展属性 aaa = wmsplanPath10keyaaaValue
//        String path10 = "wmPlans.skuInfos.extensionParameter[aaa]";
//        String val3="wmsplanPath10keyaaaValue";
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO,path10, val3);
//        System.out.println(JSONUtil.toJsonStr(orderInfoDTO));

/********************************************************************************/

        // 第十一次测试给  wmPlans[0] 的 所有sku 添加扩展属性 aaa = wmsplanPath10keyaaaValue
//        String path11 = "wmPlans[0].skuInfos.extensionParameter[aaa]";
//        String val11="wmsplanPath11keyaaaValue";
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO,path11, val11);
//        System.out.println(JSONUtil.toJsonStr(orderInfoDTO));

/********************************************************************************/

        // 第十一次测试 给 所有的wmPlans 的 skuInfos[1] 添加扩展属性 aaa = wmsplanPath12keyaaaValue
//        String path12 = "wmPlans.skuInfos[1].extensionParameter[aaa]";
//        String val12="wmsplanPath12keyaaaValue";
//        ReflectUtil.reflectAssignEvaluate(orderInfoDTO,path12, val12);
//        System.out.println(JSONUtil.toJsonStr(orderInfoDTO));

/********************************************************************************/
    }

    public static OrderInfoDTO initData() {
        SkuInfoDTO skuInfoDTO1 = new SkuInfoDTO();
        skuInfoDTO1.setGoCargoId("go01");
        skuInfoDTO1.setCustomerItemNO("no-01");

        SkuInfoDTO skuInfoDTO2 = new SkuInfoDTO();
        skuInfoDTO2.setGoCargoId("go02");
        skuInfoDTO2.setCustomerItemNO("no-02");




        Map<String, String> goOrderEXTMap1 = new HashMap<>();
        goOrderEXTMap1.put("bbb", "ccc");


        List<SkuInfoDTO> goOrderSKUInfos = new ArrayList<>();
        goOrderSKUInfos.add(skuInfoDTO1);
        goOrderSKUInfos.add(skuInfoDTO2);
        GoOrderDTO goOrderDTO = new GoOrderDTO();
        goOrderDTO.setConsignorCode("ConsignorCode01");
        goOrderDTO.setMark3("mark3");
        goOrderDTO.setI3(20);
        goOrderDTO.setSkuInfos(goOrderSKUInfos);
        goOrderDTO.setExtionParam(goOrderEXTMap1);
        // goOrder赋值完成


        SkuInfoDTO wmsSku0101 = new SkuInfoDTO();
        wmsSku0101.setGoCargoId("go03");
        wmsSku0101.setCustomerItemNO("no-03");

        SkuInfoDTO wmsSku0102 = new SkuInfoDTO();
        wmsSku0102.setGoCargoId("go04");
        wmsSku0102.setCustomerItemNO("no-04");
        Map<String, String> wmsSku0102ExtionParam = new HashMap<>();
        wmsSku0102ExtionParam.put("invNo", "abcd01");
        wmsSku0102ExtionParam.put("bu", "BM");
        wmsSku0102.setExtensionParameter(wmsSku0102ExtionParam);

        WmsPlanDTO wmsPlanDTO1 = new WmsPlanDTO();
        wmsPlanDTO1.setWarehouseType("wmsHouse1");
        wmsPlanDTO1.setWarehousedocumentType("wmsDocument1");
        wmsPlanDTO1.setSkuInfos(Arrays.asList(wmsSku0101, wmsSku0102));
        // wmsPlanDTO1赋值完成


        SkuInfoDTO wmsSku0201 = new SkuInfoDTO();
        wmsSku0201.setGoCargoId("go03");
        wmsSku0201.setCustomerItemNO("no-03");

        SkuInfoDTO wmsSku0202 = new SkuInfoDTO();
        wmsSku0202.setGoCargoId("go04");
        wmsSku0202.setCustomerItemNO("no-04");
        Map<String, String> wmsSku0202ExtionParam = new HashMap<>();
        wmsSku0202ExtionParam.put("invNo", "abcd01");
        wmsSku0202ExtionParam.put("bu", "BM");
        wmsSku0102.setExtensionParameter(wmsSku0202ExtionParam);

        WmsPlanDTO wmsPlanDTO2 = new WmsPlanDTO();
        wmsPlanDTO2.setWarehouseType("wmsHouse2");
        wmsPlanDTO2.setWarehousedocumentType("wmsDocument2");
        wmsPlanDTO2.setSkuInfos(Arrays.asList(wmsSku0201, wmsSku0202));
        // wmsPlanDTO2赋值完成


        OrderInfoDTO orderInfoDTO = new OrderInfoDTO();
        orderInfoDTO.setGoOrder(goOrderDTO);
        orderInfoDTO.setWmPlans(Arrays.asList(wmsPlanDTO1, wmsPlanDTO2));
        return orderInfoDTO;
    }
}

1.22 - 基于BigDecimal的高精度数字运算

package cn.anzhongwei.lean.demo.bigdecimal;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * 用于高精确处理常用的数学运算
 */
public class ArithmeticUtils {
    public static void main(String[] args) {
        System.out.println("2.5 to "+toCeiling(new BigDecimal(2.5)));
        System.out.println("4 to "+toCeiling(new BigDecimal(4)));
        System.out.println("4.023 to "+toCeiling(new BigDecimal(4.023)));
        System.out.println("0.5 to "+toCeiling(new BigDecimal(0.5)));
        System.out.println("-0.5 to "+toCeiling(new BigDecimal(-0.5)));
        System.out.println("-1.5 to "+toCeiling(new BigDecimal(-1.5)));
        System.out.println(new BigDecimal("1.000").abs());
        System.out.println(new BigDecimal("1").abs());
        BigDecimal b1 = new BigDecimal("5");
        BigDecimal b2 = new BigDecimal("4.000");
        System.out.println(b1.compareTo(b2));

    }
    //默认除法运算精度
    private static final int DEF_DIV_SCALE = 10;

    /**
     * 提供精确的加法运算
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */

    public static double add(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精确的加法运算
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static BigDecimal add(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.add(b2);
    }

    /**
     * 提供精确的加法运算
     *
     * @param v1    被加数
     * @param v2    加数
     * @param scale 保留scale 位小数
     * @return 两个参数的和
     */
    public static String add(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精确的减法运算
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static double sub(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.subtract(b2).doubleValue();
    }

    /**
     * 提供精确的减法运算。
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static BigDecimal sub(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.subtract(b2);
    }

    /**
     * 提供精确的减法运算
     *
     * @param v1    被减数
     * @param v2    减数
     * @param scale 保留scale 位小数
     * @return 两个参数的差
     */
    public static String sub(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static double mul(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2).doubleValue();
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static BigDecimal mul(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.multiply(b2);
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1    被乘数
     * @param v2    乘数
     * @param scale 保留scale 位小数
     * @return 两个参数的积
     */
    public static double mul(double v1, double v2, int scale) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return round(b1.multiply(b2).doubleValue(), scale);
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1    被乘数
     * @param v2    乘数
     * @param scale 保留scale 位小数
     * @return 两个参数的积
     */
    public static String mul(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
     * 小数点以后10位,以后的数字四舍五入
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */

    public static double div(double v1, double v2) {
        return div(v1, v2, DEF_DIV_SCALE);
    }

    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
     * 定精度,以后的数字四舍五入
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商
     */
    public static double div(double v1, double v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
     * 定精度,以后的数字四舍五入
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示需要精确到小数点以后几位
     * @return 两个参数的商
     */
    public static String div(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v1);
        return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精确的小数位四舍五入处理
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static double round(double v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b = new BigDecimal(Double.toString(v));
        return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    /**
     * 提供精确的小数位四舍五入处理
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static String round(String v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b = new BigDecimal(v);
        return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 取余数
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 小数点后保留几位
     * @return 余数
     */
    public static String remainder(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 取余数  BigDecimal
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 小数点后保留几位
     * @return 余数
     */
    public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 比较大小
     *
     * @param v1 被比较数
     * @param v2 比较数
     * @return 如果v1 大于v2 则 返回true 否则false
     */
    public static boolean compare(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        int bj = b1.compareTo(b2);
        boolean res;
        if (bj > 0)
            res = true;
        else
            res = false;
        return res;
    }

    public static BigDecimal toCeiling(BigDecimal val){

//        if (val.compareTo(new BigDecimal(1)) >= 0) {
//            return val.round(new MathContext(1, RoundingMode.CEILING));
//        } else {
            return val.setScale(0, RoundingMode.CEILING);
//        }
    }
}

运行结果

1.23 - 基于序列化的深拷贝

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;

import java.io.Serializable;

@AllArgsConstructor
@Data
@ToString
class Car implements Serializable {
    private static final long serialVersionUID = -5713945027627603702L;

    private String brand;       // 品牌
    private int maxSpeed;       // 最高时速
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;

import java.io.Serializable;

@AllArgsConstructor
@Data
@ToString
class Person implements Serializable {
    private static final long serialVersionUID = -9102017020286042305L;

    private String name;    // 姓名
    private int age;        // 年龄
    private Car car;        // 座驾
}

import java.io.*;

/**
 * 浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝(例:assign())
 *
 * 深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse()和JSON.stringify(),但是此方法无法复制函数类型)
 */
public class CloneUtil {

    public static void main(String[] args) {
        try {
            Person p1 = new Person("郭靖", 33, new Car("Benz", 300));
            Person p2 = CloneUtil.clone(p1);   // 深度克隆
            p2.getCar().setBrand("BYD");
            // 修改克隆的Person对象p2关联的汽车对象的品牌属性
            // 原来的Person对象p1关联的汽车不会受到任何影响
            // 因为在克隆Person对象时其关联的汽车对象也被克隆了
            System.out.println(p1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //  私有构造方法
    private CloneUtil() {
        throw new AssertionError();
    }

    /*
    注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,
    可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,
    这种是方案明显优于使用Object类的clone方法克隆对象。
    让问题在编译的时候暴露出来总是好过把问题留到运行时。
     */
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj) throws Exception {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        oos.writeObject(obj);

        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);
        return (T) ois.readObject();

        // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
        // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
    }
}

1.24 - 时间日期

package cn.anzhongwei.lean.demo.date;


import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;

public class DateUtils {
    private static final DateTimeFormatter YEAH_MONTH_DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter YEAH_MONTH_DAY_HOUR_MINUTE_SECOND = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public DateUtils() {
    }

    public static void main(String[] args) {

        System.out.println("1. 获取当前日期字符串: " + getNowDateTime());
//
        System.out.println("2.1 获取UTC时间: " + getUTCTime());

        System.out.println("2.2 获取时间戳: " + getTimestamp());
//
        System.out.println("3. 根据字符串日期转LocalDate: " + getString2LocalDate("2019-01-01"));

        System.out.println("4. 根据字符串日期时间转 LocalDateTime: " + getString2FullDate("2019-01-01 00:00:00"));

        System.out.println("5. 根据自定义格式获取当前日期: " + getNowDateByFormatter("yyyy++MM++dd"));

        System.out.println("6. 根据自定义格式获取当前日期时间: " + getNowDateTimeByFormatter("yyyy++MM++dd HH:mm:ss"));

        System.out.printf(
                """
                            7. 根据节点获取当前日期时间信息
                            %s 年 %s 月 %s 日 星期 %s 是今年的第%s天
                            %s时 %s分 %s秒
                        %n""", getNowDateNode(DateEnum.YEAR),
                getNowDateNode(DateEnum.MONTH),
                getNowDateNode(DateEnum.DAY),
                getNowDateNode(DateEnum.WEEK),
                getNowDateNode(DateEnum.DAYOFYEAR),
                getNowDateNode(DateEnum.HOUR),
                getNowDateNode(DateEnum.MINUTE),
                getNowDateNode(DateEnum.SECOND)
        );

        System.out.println("8. 一年后的日期时间: " + getPreOrAfterDateTime(DateEnum.YEAR, 1));
        System.out.println("8. 一年前的日期时间: " + getPreOrAfterDateTime(DateEnum.YEAR, -1));

        System.out.println("8. 一小时后的日期时间: " + getPreOrAfterDateTime(DateEnum.HOUR, 1));
        System.out.println("8. 一小时前的日期时间: " + getPreOrAfterDateTime(DateEnum.HOUR, -1));

        System.out.println("9. 比较两个日期时间大小 2019-01-01 00:00:00  & 2019-01-01 00:00:01: " + compareDateTime("2019-01-01 00:00:00", "2019-01-01 00:00:01"));

        System.out.println("10. 判断是否闰年: " + isLeapYear("2024-01-01"));

        System.out.println("11. 两个日期间隔天数: " + getPeridNum("2019-01-01", "2019-01-01", DateEnum.DAY));

        System.out.println("12. 获取当前时区:" + getTimeZone());

        System.out.println("13. 获取America/Los_Angeles时区时间 ZoneId.SHORT_IDS:" + getTimeZoneDateTime("America/Los_Angeles"));
    }



    // 1. 获取当前日期时间字符串
    // 改成LocalDate.now().toString() 获取当前日期字符串
    // 改成LocalTime.now().toString() 获取当前时间字符串
    public static String getNowDateTime() {
        return LocalDateTime.now().format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
    }

    // 2.1 获得utd时间
    public static String getUTCTime() {
        //Clock类
        return Clock.systemUTC().instant().toString();
    }

    // 2.2 获得时间戳
    public static Long getTimestamp() {
        //Clock类
        return Clock.systemUTC().millis();
    }

    // 3. 根据字符串日期转LocalDate
    public static LocalDate getString2LocalDate(String date) {
        return LocalDate.parse(date, YEAH_MONTH_DAY);
    }

    // 4. 根据字符串日期时间转 LocalDateTime
    public static LocalDateTime getString2FullDate(String date) {
        return LocalDateTime.parse(date, YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
    }

    // 5. 根据自定义格式获取当前日期
    public static String getNowDateByFormatter(String formatterPattern) {
        return LocalDate.now().format(DateTimeFormatter.ofPattern(formatterPattern));
    }

    // 6. 根据自定义格式获取当前日期时间
    public static String getNowDateTimeByFormatter(String formatterPattern) {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(formatterPattern));
    }

    // 7. 获取当前 节点
    public static Integer getNowDateNode(DateEnum dateEnum) {
        LocalDateTime nowDate = LocalDateTime.now();
        Integer nowNode = null;
        switch (dateEnum) {
            case YEAR:
                nowNode = nowDate.getYear();
                break;
            case MONTH:
                nowNode = nowDate.getMonthValue();
                break;
            case WEEK:
                nowNode = conversionWeek2Num(DateUtils.WeekEnum.valueOf(nowDate.getDayOfWeek().toString()));
                break;
            case DAY:
                nowNode = nowDate.getDayOfMonth();
                break;
            case DAYOFYEAR:
                nowNode = nowDate.getDayOfYear();
                break;
            case HOUR:
                nowNode = nowDate.getHour();
                break;
            case MINUTE:
                nowNode = nowDate.getMinute();
                break;
            case SECOND:
                nowNode = nowDate.getSecond();
        }

        return nowNode;
    }

    // 8. 从当前日期加或减一个 年 月 日 周 时 分 秒 的时间 加就是正数 减就是负数
    // LocalDateTime nowDate = LocalDateTime.now();
    // 改成 LocalDate nowDate = LocalDate.now(); 就是只能获取 年月日周
    // 改成 LocalTime nowDate = LocalTime.now(); 就是只能获取 时 分 秒
    public static String getPreOrAfterDateTime(DateEnum dateEnum, long time) {
        LocalDateTime nowDate = LocalDateTime.now();
        switch (dateEnum) {
            case YEAR:
                // 从api 的角度也可以用 这个api提供的时间单位更多
//                nowDate.plus(time, ChronoUnit.YEARS);
//                nowDate.minus(time, ChronoUnit.YEARS)
                return nowDate.plusYears(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
            case MONTH:
                return nowDate.plusMonths(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
            case WEEK:
                return nowDate.plusWeeks(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
            case DAY:
                return nowDate.plusDays(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
            case DAYOFYEAR:
                throw new RuntimeException("不支持的选项");
            case HOUR:
                return nowDate.plusHours(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
            case MINUTE:
                return nowDate.plusMinutes(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
            case SECOND:
                return nowDate.plusSeconds(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
            default:
                throw new RuntimeException("不支持的选项");
        }
    }

    // 9. 比较日期时间 begin 比 end 早 返回true
    // 同理LocalDate比较日期  LocalTime 比较时间
    public static boolean compareDateTime(String begin, String end) {
        LocalDateTime beginDate = LocalDateTime.parse(begin, YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
        LocalDateTime endDate = LocalDateTime.parse(end, YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
        return beginDate.isBefore(endDate);
    }

    // 10. 判断是否闰年
    public static boolean isLeapYear(String date) {
        return LocalDate.parse(date.trim()).isLeapYear();
    }

    // 11. 获取两个日期之间的间隔 这个函数只能用LocalDate
    public static Integer getPeridNum(String begin, String end, DateEnum dateEnum) {
        switch (dateEnum) {
            case YEAR:
                return Period.between(LocalDate.parse(begin), LocalDate.parse(end)).getYears();
            case MONTH:
                return Period.between(LocalDate.parse(begin), LocalDate.parse(end)).getMonths();
            case WEEK:
                throw new RuntimeException("不支持的参数");
            case DAY:
                return Period.between(LocalDate.parse(begin), LocalDate.parse(end)).getDays();
            default:
                throw new RuntimeException("不支持的参数");
        }
    }

    // 12. 获取当前时区
    public static String getTimeZone() {
        return ZoneId.systemDefault().toString();
    }

    // 13. 获取指定时区时间 ZoneId.SHORT_IDS
    public static String getTimeZoneDateTime(String timeZone) {
        return LocalDateTime.now(ZoneId.of(timeZone)).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
    }




    public static int conversionWeek2Num(WeekEnum weekEnum) {
        switch (weekEnum) {
            case MONDAY:
                return 1;
            case TUESDAY:
                return 2;
            case WEDNESDAY:
                return 3;
            case THURSDAY:
                return 4;
            case FRIDAY:
                return 5;
            case SATURDAY:
                return 6;
            case SUNDAY:
                return 7;
            default:
                return -1;
        }
    }

    public static enum WeekEnum {
        MONDAY,
        TUESDAY,
        WEDNESDAY,
        THURSDAY,
        FRIDAY,
        SATURDAY,
        SUNDAY;

        private WeekEnum() {
        }
    }

    public static enum DateEnum {
        YEAR,
        MONTH,
        WEEK,
        DAY,
        HOUR,
        MINUTE,
        SECOND,
        DAYOFYEAR;

        private DateEnum() {
        }
    }
}

1.25 - 使用抽象类来实现接口

  1. 创建接口类
package cn.anzhongwei.lean.demo.abstractImplementInterface;

import java.math.BigDecimal;

/**
 * 接口中定义3个方法
 */
public interface InterfaceA {
    int a();
    String b();
    BigDecimal c();
}
  1. 创建抽象类
package cn.anzhongwei.lean.demo.abstractImplementInterface;

/**
 * 抽象中去实现两个方法,只有c()没有在抽象类中实现,留给最后类去实现c
 *
 */
public abstract class AbstractB implements InterfaceA{
    @Override
    public int a() {
        return 1;
    }

    @Override
    public String b() {
        return "Hello World!";
    }

}
  1. 创建实现类
package cn.anzhongwei.lean.demo.abstractImplementInterface;

import java.math.BigDecimal;

/**
 * 由类来实现抽象中未实现得方法, 这样得好处是可以将抽象当成通用得实现,每个最终得实现只去实现差异即可,
 * 既有通用得又有灵活的方法
 */
public class ClassC extends AbstractB{
    @Override
    public BigDecimal c() {
        return new BigDecimal(20);
    }
}
  1. 测试类
package cn.anzhongwei.lean.demo.abstractImplementInterface;

public class TestMain {
    public static void main(String[] args) {
        InterfaceA interfaceA1 = new ClassC();
        System.out.println(interfaceA1.a()); // 1
        System.out.println(interfaceA1.b()); // Hello World!
        System.out.println(interfaceA1.c()); // 20


        AbstractB abstractB2 = new ClassC();
        System.out.println(abstractB2.a()); // 1
        System.out.println(abstractB2.b()); // Hello World!
        System.out.println(abstractB2.c()); // 20

        ClassC classc3 = new ClassC();
        System.out.println(classc3.a()); // 1
        System.out.println(classc3.b()); // Hello World!
        System.out.println(classc3.c()); // 20
    }
}

1.26 - 文件拷贝


import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class NIOFileReadAndWrite {
    private String file;

    public String getFile() {
        return file;
    }

    public void setFile(String file) {
        this.file = file;
    }

    public NIOFileReadAndWrite(String file) throws IOException {
        super();
        this.file = file;
    }

    /**
     * NIO读取文件
     * @param allocate
     * @throws IOException
     */
    public void read(int allocate) throws IOException {

        RandomAccessFile access = new RandomAccessFile(this.file, "r");

        //FileInputStream inputStream = new FileInputStream(this.file);
        FileChannel channel = access.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(allocate);

        CharBuffer charBuffer = CharBuffer.allocate(allocate);
        Charset charset = Charset.forName("GBK");
        CharsetDecoder decoder = charset.newDecoder();
        int length = channel.read(byteBuffer);
        while (length != -1) {
            byteBuffer.flip();
            decoder.decode(byteBuffer, charBuffer, true);
            charBuffer.flip();
            System.out.println(charBuffer.toString());
            // 清空缓存
            byteBuffer.clear();
            charBuffer.clear();
            // 再次读取文本内容
            length = channel.read(byteBuffer);
        }
        channel.close();
        if (access != null) {
            access.close();
        }
    }

    /**
     * NIO写文件
     * @param context
     * @param allocate
     * @param chartName
     * @throws IOException
     */
    public void write(String context, int allocate, String chartName) throws IOException{
        // FileOutputStream outputStream = new FileOutputStream(this.file); //文件内容覆盖模式 --不推荐
        FileOutputStream outputStream = new FileOutputStream(this.file, true); //文件内容追加模式--推荐
        FileChannel channel = outputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(allocate);
        byteBuffer.put(context.getBytes(chartName));
        byteBuffer.flip();//读取模式转换为写入模式
        channel.write(byteBuffer);
        channel.close();
        if(outputStream != null){
            outputStream.close();
        }
    }

    /**
     * nio事实现文件拷贝
     * @param source
     * @param target
     * @param allocate
     * @throws IOException
     */
    public static void nioCpoy(String source, String target, int allocate) throws IOException{
        ByteBuffer byteBuffer = ByteBuffer.allocate(allocate);
        FileInputStream inputStream = new FileInputStream(source);
        FileChannel inChannel = inputStream.getChannel();

        FileOutputStream outputStream = new FileOutputStream(target);
        FileChannel outChannel = outputStream.getChannel();

        int length = inChannel.read(byteBuffer);
        while(length != -1){
            byteBuffer.flip();//读取模式转换写入模式
            outChannel.write(byteBuffer);
            byteBuffer.clear(); //清空缓存,等待下次写入
            // 再次读取文本内容
            length = inChannel.read(byteBuffer);
        }
        outputStream.close();
        outChannel.close();
        inputStream.close();
        inChannel.close();
    }

    public static void fileChannelCopy(String sfPath, String tfPath) {

        File sf = new File(sfPath);
        File tf = new File(tfPath);
        FileInputStream fi = null;
        FileOutputStream fo = null;
        FileChannel in = null;
        FileChannel out = null;
        try{
            fi = new FileInputStream(sf);
            fo = new FileOutputStream(tf);
            in = fi.getChannel();//得到对应的文件通道
            out = fo.getChannel();//得到对应的文件通道
            in.transferTo(0, in.size(), out);//连接两个通道,并且从in通道读取,然后写入out通道
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try{
                fi.close();
                in.close();
                fo.close();
                out.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }


    //IO方法实现文件k拷贝
    private static void traditionalCopy(String sourcePath, String destPath) throws Exception {
        File source = new File(sourcePath);
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        FileInputStream fis = new FileInputStream(source);
        FileOutputStream fos = new FileOutputStream(dest);
        byte[] buf = new byte[1024];
        int len = 0;
        while ((len = fis.read(buf)) != -1) {
            fos.write(buf, 0, len);
        }
        fis.close();
        fos.close();
    }

    public static void main(String[] args) throws Exception{


        long start = System.currentTimeMillis();
        nioCpoy("D:\\迅雷下载\\jdk-21_windows-x64_bin.zip", "D:\\qwe\\jdk-21_windows-x64_bin.zip",10240);
        long end = System.currentTimeMillis();
        System.out.println("用时为:" + (end-start));

        long start2 = System.currentTimeMillis();
        fileChannelCopy("D:\\迅雷下载\\jdk-21_windows-x64_bin.zip", "D:\\qwe\\jdk-21_windows-x64_bin.zip");
        long end2 = System.currentTimeMillis();
        System.out.println("用时为:" + (end2-start2));

        long start3 = System.currentTimeMillis();
        traditionalCopy("D:\\迅雷下载\\jdk-21_windows-x64_bin.zip", "D:\\qwe\\jdk-21_windows-x64_bin.zip");
        long end3 = System.currentTimeMillis();
        System.out.println("用时为:" + (end3-start3));
    }

}

1.27 - 下载图片

package cn.anzhongwei.lean.demo.downfileforweb;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

@RestController
@ResponseBody
public class DownImgController {

    //http://localhost:8080/ttpng?seriesUniqueCode=1
    @GetMapping(value = "/writeImg1")
    public void writeImg1(HttpServletResponse response) throws IOException {
        //设置响应头信息需要在输出流之前, 就可以触发浏览器下载机制, 注释下列3行或将其写在输出流之后, 都只会在浏览器显示不会触发下载
//        response.setHeader("Content-disposition", "attachment;filename=cargoExport.jpg");
//        response.setContentType("multipart/form-data");
//        response.setCharacterEncoding(StandardCharsets.UTF_8.name());

        URL url = null;
        InputStream is = null;
        ByteArrayOutputStream outStream = null;
        HttpURLConnection httpUrl = null;
        //随便找一张图片
        url = new URL("http://img.ay1.cc/uploads/20221114/9f3e884272b562cf8b27a048c3634621.jpg");
        httpUrl = (HttpURLConnection) url.openConnection();
        httpUrl.connect();
        httpUrl.getInputStream();
        is = httpUrl.getInputStream();
        outStream = new ByteArrayOutputStream();
        //创建一个Buffer字符串
        byte[] buffer = new byte[1024];
        //每次读取的字符串长度,如果为-1,代表全部读取完毕
        int len = 0;
        //使用一个输入流从buffer里把数据读取出来
        while ((len = is.read(buffer)) != -1) {
            //用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
            response.getOutputStream().write(buffer, 0, len);
        }

        response.flushBuffer();
    }


    @GetMapping(value = "/writeImg2", produces = MediaType.IMAGE_JPEG_VALUE)
    public void writeImg2(HttpServletResponse response) throws IOException {
        URL url = null;
        InputStream is = null;
        ByteArrayOutputStream outStream = null;
        HttpURLConnection httpUrl = null;

        url = new URL("http://img.ay1.cc/uploads/20221114/9f3e884272b562cf8b27a048c3634621.jpg");
        httpUrl = (HttpURLConnection) url.openConnection();
        httpUrl.connect();
        httpUrl.getInputStream();
        is = httpUrl.getInputStream();
        outStream = new ByteArrayOutputStream();
        //创建一个Buffer字符串
        byte[] buffer = new byte[1024];
        //每次读取的字符串长度,如果为-1,代表全部读取完毕
        int len = 0;
        //使用一个输入流从buffer里把数据读取出来
        while ((len = is.read(buffer)) != -1) {
            //用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
            outStream.write(buffer, 0, len);
        }
        byte[] temp = outStream.toByteArray();

        response.setHeader("Content-disposition", "attachment;filename=cargoExport.jpg");
        response.setContentType("multipart/form-data");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getOutputStream().write(temp);
        response.flushBuffer();
    }


    public void imgToBase64() {
        URL downUrl = new URL("http://localhost/test.png");
        HttpURLConnection conn = (HttpURLConnection) downUrl.openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5000);
        InputStream inputStream = conn.getInputStream();

        ByteArrayOutputStream data = new ByteArrayOutputStream();
        byte[] by = new byte[1024];
        int len = -1;
        while((len = inputStream.read(by)) != -1){
            data.write(by,0,len);
        }
        inputStream.close();
        Base64.getEncoder().encode(data.toByteArray());
        String imgStr = Base64.getEncoder().encodeToString(data.toByteArray());
        System.out.println(imgStr);
    }
}

1.28 - 原生Java发送Post请求

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class JavaPostJson2 {
    final static String url = "http://localhost:1111/sendData";
    // final static String params = "{\"token\":\"12345\","
    //         + "\"appId\":\"20180628173051169c85d965d164ed9ab1281d22ff350ec19\"" 
    //         + "\"appName\":\"test33\""
    //         + "\"classTopic\":\"112\"" 
    //         + "\"eventTag\":\"22\"" 
    //         + "\"msg\":{}"
    //         + "}";
    final static String params = "{\"token\":\"12345\","
            + "\"appId\":\"20180628173051169c85d965d164ed9ab1281d22ff350ec19\","
            + "\"appName\":\"test33\","
            + "\"classTopic\":\"112\","
            + "\"eventTag\":\"22\""
            + "}";

    public static void main(String[] args) {

        String res = post(url, params);
        System.out.println(res);

    }

    /**
     * 发送HttpPost请求
     * 
     * @param strURL
     *            服务地址
     * @param params
     *            json字符串,例如: "{ \"id\":\"12345\" }" ;其中属性名必须带双引号<br/>
     * @return 成功:返回json字符串<br/>
     */
    public static String post(String strURL, String params) {
        System.out.println(strURL);
        System.out.println(params);
        BufferedReader reader = null;
        try {
            URL url = new URL(strURL);// 创建连接
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setInstanceFollowRedirects(true);
            connection.setRequestMethod("POST"); // 设置请求方式
            // connection.setRequestProperty("Accept", "application/json"); // 设置接收数据的格式
            connection.setRequestProperty("Content-Type", "application/json"); // 设置发送数据的格式
            connection.connect();
            OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), "UTF-8"); // utf-8编码
            out.append(params);
            out.flush();
            out.close();
            // 读取响应
            reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
            String line;
            String res = "";
            while ((line = reader.readLine()) != null) {
                res += line;
            }
            reader.close();

            return res;
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return "error"; // 自定义错误信息
    }

}

2 - Netty实战

Netty实战,小傅哥博客Netty实战的实现

代码地址

案例一 TCP服务端

要求1: 在客户端连接后打印客户端端的ip和端口号

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class DemoNetty101 {
    public static void main(String[] args) {
        DemoNetty101 demoNetty101 = new DemoNetty101();
        demoNetty101.startTCP();
    }

    private void startTCP() {
        // 一个监听工作组,在很多写法中叫做master组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 一个工作监听组,在很多写法中交child组
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap server = new ServerBootstrap();
            server.group(bossGroup, workerGroup)
                    // 设置非阻塞模式
                    .channel(NioServerSocketChannel.class) 
                    // 添加自定义处理器
                    .childHandler(new TCPServerInitializer());
            ChannelFuture f = server.bind(8088).sync();
            System.out.println("Http Server started, Listening on " + 8088);
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;

public class TCPServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        System.out.println("初始化连接成功");
        System.out.println(socketChannel.remoteAddress().getHostString()+":"+socketChannel.remoteAddress().getPort());
    }
}

案例二 TCP服务端

要求1: 在客户端连接后打印客户端端的ip和端口号 要求2:能打印出客户端发送的内容

案例三 TCP服务端

要求1: 在客户端连接后打印客户端端的ip和端口号 要求2: 使用内置的协议解析器解码客户端发送的内容 要求3:使用字符串解码器解码, 打印出客户端发送的内容

案例四 TCP服务端

要求1: 让入站数据经过多个Handle处理器 要求2: 每个处理器都知道客户端关闭了连接

案例五 TCP服务端

要求1: 能将收到的内容原样回复给客户端程序(这里的变化主要是要将返回的数据进行编码)

案例六 TCP服务端

要求1: 使用内置的字符串编解码器

案例七 TCP服务端

要求1: 接收多个客户端链接(其实就是可以缓存多个客户端链接),并每次响应给所有客户端(群发消息)

案例八 TCP客户端

要求1: 新建客户端能正常连接到一个服务端

案例九 TCP客户端

要求1: 新建客户端能正常连接到一个服务端 要求2: 使用netty自带的换行解析器拆分数据, 并使用字符串解码编码器来打印服务端信息和返回服务端字符串

案例十 TCP客户端和服务端

要求1: 使用继承ChannelInboundHandlerAdapter处理入站信息 使用继承ChannelOutboundHandlerAdapter处理出站时间

@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
        System.out.println("链接报告开始");
        System.out.println("链接报告信息:本客户端链接到服务端。channelId:" + socketChannel.id());
        System.out.println("链接报告完毕");
        socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024))

        .addLast(new StringDecoder(Charset.forName("GBK")))
        .addLast(new StringEncoder(Charset.forName("GBK")))

        .addLast(new Outbound1())
        .addLast(new Outbound2())

        .addLast(new Inbound1())
        //必须使用ctx.writeAndFlush(message); 否则出站处理器收不到消息,
        //如果定义到Outbound前面相当于数据提前掉头了
        .addLast(new Inbound2())
        ;
        }
//在Inbound2中必须使用ctx.writeAndFlush(message); 也就是最后一个入站处理器必须使用ctx.writeAndFlush(message); 否则出站处理器收不到消息
// 同时如果在Inbound1中使用了ctx.writeAndFlush(message); 那么数据将不再向Inbound2中传递
//根据处理器执行顺序,数据会先走Outbound2 再走Outbound1, 所以再Outbund1中必须调用ctx.writeAndFlush(message, promise);

案例十一 UDP客户端和服务端

案例十二 简单的http服务器

3 - Spring

以SpringBoot 3 的支持

3.1 - GetSpringBeanUtil


import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

/**
 * 可以实现在普通类中获取springBean实例
 */
@Component
public final class GetSpringBeanUtil  implements BeanFactoryPostProcessor {
    private static ConfigurableListableBeanFactory beanFactory;

    public GetSpringBeanUtil() {
    }

    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        GetSpringBeanUtil.beanFactory = beanFactory;
    }

    public static <T> T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }

    public static <T> T getBean(Class<T> clz) throws BeansException {
        T result = beanFactory.getBean(clz);
        return result;
    }

    public static boolean containsBean(String name) {
        return beanFactory.containsBean(name);
    }

    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.isSingleton(name);
    }

    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getType(name);
    }

    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getAliases(name);
    }

    public static <T> T getAopProxy(T invoker) {
        return (T) AopContext.currentProxy();
    }
}

4 - 一文读懂classLoader

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习ClassLoader这种加载机制。

备注:本文篇幅比较长,但内容简单,大家不要恐慌,安静地耐心翻阅就是

Class文件的认识

我们都知道在Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的。如我们编写一个简单的程序HelloWorld.java

public class HelloWorld{

    public static void main(String[] args){
        System.out.println("Hello world!");
    }
}

如图:
这里写图片描述
然后,我们需要在命令行中进行java文件的编译

javac HelloWorld.java

这里写图片描述
可以看到目录下生成了.class文件

我们再从命令行中执行命令:

java HelloWorld

这里写图片描述

上面是基本代码示例,是所有入门JAVA语言时都学过的东西,这里重新拿出来是想让大家将焦点回到class文件上,class文件是字节码格式文件,java虚拟机并不能直接识别我们平常编写的.java源文件,所以需要javac这个命令转换成.class文件。另外,如果用C或者PYTHON编写的程序正确转换成.class文件后,java虚拟机也是可以识别运行的。更多信息大家可以参考这篇

了解了.class文件后,我们再来思考下,我们平常在Eclipse中编写的java程序是如何运行的,也就是我们自己编写的各种类是如何被加载到jvm(java虚拟机)中去的。

你还记得java环境变量吗?

初学java的时候,最害怕的就是下载JDK后要配置环境变量了,关键是当时不理解,所以战战兢兢地照着书籍上或者是网络上的介绍进行操作。然后下次再弄的时候,又忘记了而且是必忘。当时,心里的想法很气愤的,想着是–这东西一点也不人性化,为什么非要自己配置环境变量呢?太不照顾菜鸟和新手了,很多菜鸟就是因为卡在环境变量的配置上,遭受了太多的挫败感。

因为我是在Windows下编程的,所以只讲Window平台上的环境变量,主要有3个:JAVA_HOMEPATHCLASSPATH

JAVA_HOME

指的是你JDK安装的位置,一般默认安装在C盘,如

C:\Program Files\Java\jdk1.8.0_91

PATH

将程序路径包含在PATH当中后,在命令行窗口就可以直接键入它的名字了,而不再需要键入它的全路径,比如上面代码中我用的到javacjava两个命令。
一般的

PATH=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%PATH%;

也就是在原来的PATH路径上添加JDK目录下的bin目录和jre目录的bin.

CLASSPATH

CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar

一看就是指向jar包路径。
需要注意的是前面的.;.代表当前目录。

环境变量的设置与查看

设置可以右击我的电脑,然后点击属性,再点击高级,然后点击环境变量,具体不明白的自行查阅文档。

查看的话可以打开命令行窗口


echo %JAVA_HOME%

echo %PATH%

echo %CLASSPATH%

好了,扯远了,知道了环境变量,特别是CLASSPATH时,我们进入今天的主题Classloader.

JAVA类加载流程

Java语言系统自带有三个类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
  • Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
  • Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。

我们上面简单介绍了3个ClassLoader。说明了它们加载的路径。并且还提到了-Xbootclasspath-D java.ext.dirs这两个虚拟机参数选项。

加载顺序?

我们看到了系统的3个类加载器,但我们可能不知道具体哪个先行呢?
我可以先告诉你答案

  1. Bootstrap CLassloder
  2. Extention ClassLoader
  3. AppClassLoader

为了更好的理解,我们可以查看源码。
sun.misc.Launcher,它是一个java虚拟机的入口应用。

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(loader);
    }

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {}

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {}

源码有精简,我们可以得到相关的信息。

  1. Launcher初始化了ExtClassLoader和AppClassLoader。
  2. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty("sun.boot.class.path")得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。

我们可以先代码测试一下sun.boot.class.path是什么内容。

System.out.println(System.getProperty("sun.boot.class.path"));

得到的结果是:

C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
C:\Program Files\Java\jre1.8.0_91\classes

可以看到,这些全是JRE目录下的jar包或者是class文件。

ExtClassLoader源码

如果你有足够的好奇心,你应该会对它的源码感兴趣

/*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {

        static {
            ClassLoader.registerAsParallelCapable();
        }

        /**
         * create an ExtClassLoader. The ExtClassLoader is created
         * within a context that limits which files it can read
         */
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }

        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            if (s != null) {
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                for (int i = 0; i < count; i++) {
                    dirs[i] = new File(st.nextToken());
                }
            } else {
                dirs = new File[0];
            }
            return dirs;
        }
 
......
    }

我们先前的内容有说过,可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。这里我们通过可以编写测试代码。

System.out.println(System.getProperty("java.ext.dirs"));

结果如下:

C:\Program Files\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext

AppClassLoader源码

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {


        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);

     
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

        ......
    }

可以看到AppClassLoader加载的就是java.class.path下的路径。我们同样打印它的值。

System.out.println(System.getProperty("java.class.path"));

结果:

D:\workspace\ClassLoaderDemo\bin

这个路径其实就是当前java工程目录bin,里面存放的是编译生成的class文件。

好了,自此我们已经知道了BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.pathjava.ext.dirsjava.class.path来加载资源文件的。

接下来我们探讨它们的加载顺序,我们先用Eclipse建立一个java工程。
这里写图片描述
然后创建一个Test.java文件。

public class Test{}

然后,编写一个ClassLoaderTest.java文件。


public class ClassLoaderTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
    
        ClassLoader cl = Test.class.getClassLoader();
        
        System.out.println("ClassLoader is:"+cl.toString());
        
    }

}

我们获取到了Test.class文件的类加载器,然后打印出来。结果是:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93  

也就是说明Test.class文件是由AppClassLoader加载的。

这个Test类是我们自己编写的,那么int.class或者是String.class的加载是由谁完成的呢?
我们可以在代码中尝试

public class ClassLoaderTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
    
        ClassLoader cl = Test.class.getClassLoader();
        
        System.out.println("ClassLoader is:"+cl.toString());
        
        cl = int.class.getClassLoader();
        
        System.out.println("ClassLoader is:"+cl.toString());
        
    }

}

运行一下,却报错了

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" java.lang.NullPointerException
    at ClassLoaderTest.main(ClassLoaderTest.java:15)

提示的是空指针,意思是int.class这类基础类没有类加载器加载?

当然不是!
int.class是由Bootstrap ClassLoader加载的。要想弄明白这些,我们首先得知道一个前提。

每个类加载器都有一个父加载器

每个类加载器都有一个父加载器,比如加载Test.class是由AppClassLoader完成,那么AppClassLoader也有一个父加载器,怎么样获取呢?很简单,通过getParent方法。比如代码可以这样编写:

ClassLoader cl = Test.class.getClassLoader();
        
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());

运行结果如下:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742

这个说明,AppClassLoader的父加载器是ExtClassLoader。那么ExtClassLoader的父加载器又是谁呢?

System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString());

运行如果:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742
java.lang.NullPointerException
    at ClassLoaderTest.main(ClassLoaderTest.java:13)

又是一个空指针异常,这表明ExtClassLoader也没有父加载器。那么,为什么标题又是每一个加载器都有一个父加载器呢?这不矛盾吗?为了解释这一点,我们还需要看下面的一个基础前提。

父加载器不是父类

我们先前已经粘贴了ExtClassLoader和AppClassLoader的代码。

static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}

可以看见ExtClassLoader和AppClassLoader同样继承自URLClassLoader,但上面一小节代码中,为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢?先从URLClassLoader说起,这个类又是什么?
先上一张类的继承关系图
这里写图片描述

URLClassLoader的源码中并没有找到getParent()方法。这个方法在ClassLoader.java中。

public abstract class ClassLoader {

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
    // @GuardedBy("ClassLoader.class")
private static ClassLoader scl;

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    ...
}
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
    if (parent == null)
        return null;
    return parent;
}
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    return scl;
}

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            //通过Launcher获取ClassLoader
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}
}

我们可以看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:

  1. 由外部类创建ClassLoader时直接指定一个ClassLoader为parent。
  2. getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

我们主要研究的是ExtClassLoader与AppClassLoader的parent的来源,正好它们与Launcher类有关,我们上面已经粘贴过Launcher的部分代码。

public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
        //将ExtClassLoader对象实例传递进去
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

public ClassLoader getClassLoader() {
        return loader;
    }
static class ExtClassLoader extends URLClassLoader {

        /**
         * create an ExtClassLoader. The ExtClassLoader is created
         * within a context that limits which files it can read
         */
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            //ExtClassLoader在这里创建
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }


        /*
         * Creates a new ExtClassLoader for the specified directories.
         */
        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);
           
        }
        }
 }

我们需要注意的是

ClassLoader extcl;
        
extcl = ExtClassLoader.getExtClassLoader();

loader = AppClassLoader.getAppClassLoader(extcl);

代码已经说明了问题AppClassLoader的parent是一个ExtClassLoader实例。

ExtClassLoader并没有直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。

public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);   
}

对应的代码

public  URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
     super(parent);
}

答案已经很明了了,ExtClassLoader的parent为null。

上面张贴这么多代码也是为了说明AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null。这符合我们之前编写的测试代码。

不过,细心的同学发现,还是有疑问的我们只看到ExtClassLoader和AppClassLoader的创建,那么BootstrapClassLoader呢?

还有,ExtClassLoader的父加载器为null,但是Bootstrap CLassLoader却可以当成它的父加载器这又是为何呢?

我们继续往下进行。

Bootstrap ClassLoader是由C++编写的。

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象。具体是什么原因,很快就知道答案了。

双亲委托

双亲委托。
我们终于来到了这一步了。
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
整个流程可以如下图所示:
这里写图片描述
这张图是用时序图画出来的,不过画出来的结果我却自己都觉得不理想。

大家可以看到2根箭头,蓝色的代表类加载器向上委托的方向,如果当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。直到Bootstrap ClassLoader。如果Bootstrap ClassLoader也没有加载过此class实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器,也就是ExtClassLoader,这样类似操作直到终点,也就是我上图中的红色箭头示例。
用序列描述一下:

  1. 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
  2. 递归,重复第1部的操作。
  3. 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
  4. Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
  5. ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。

我说过上面用时序图画的让自己不满意,现在用框图,最原始的方法再画一次。
这里写图片描述

上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

重要方法

loadClass()

JDK文档中是这样写的,通过指定的全限定类名加载class,它通过同名的loadClass(String,boolean)方法。

protected Class<?> loadClass(String name,
                             boolean resolve)
                      throws ClassNotFoundException

上面是方法原型,一般实现这个方法的步骤是

  1. 执行findLoadedClass(String)去检测这个class是不是已经加载过了。
  2. 执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
  3. 如果向上委托父加载器没有加载成功,则通过findClass(String)查找。

如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。 我们可以从源代码看出这个步骤。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检测是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //父加载器不为空则调用父加载器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空则调用Bootstrap Classloader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //父加载器没有找到,则调用findclass
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //调用resolveClass()
                resolveClass(c);
            }
            return c;
        }
    }

代码解释了双亲委托。

另外,要注意的是如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。
另外

if (parent != null) {
    //父加载器不为空则调用父加载器的loadClass
    c = parent.loadClass(name, false);
} else {
    //父加载器为空则调用Bootstrap Classloader
    c = findBootstrapClassOrNull(name);
}

前面说过ExtClassLoader的parent为null,所以它向上委托时,系统会为它指定Bootstrap ClassLoader。

自定义ClassLoader

不知道大家有没有发现,不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?

如果要这样做的话,需要我们自定义一个classloader。

自定义步骤

  1. 编写一个类继承自ClassLoader抽象类。
  2. 复写它的findClass()方法。
  3. findClass()方法中调用defineClass()

defineClass()

这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。

注意点:

**一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。 **

上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。

自定义ClassLoader示例之DiskClassLoader。

假设我们需要一个自定义的classloader,默认加载路径为D:\lib下的jar包和资源。

我们写编写一个测试用的类文件,Test.java

Test.java

package com.frank.test;

public class Test {
    
    public void say(){
        System.out.println("Say Hello");
    }

}

然后将它编译过年class文件Test.class放到D:\lib这个路径下。

DiskClassLoader

我们编写DiskClassLoader的代码。

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;


public class DiskClassLoader extends ClassLoader {
    
    private String mLibPath;
    
    public DiskClassLoader(String path) {
        // TODO Auto-generated constructor stub
        mLibPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // TODO Auto-generated method stub
        
        String fileName = getFileName(name);
        
        File file = new File(mLibPath,fileName);
        
        try {
            FileInputStream is = new FileInputStream(file);
            
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            
            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            
            return defineClass(name,data,0,data.length);
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return super.findClass(name);
    }

    //获取要加载 的class文件名
    private String getFileName(String name) {
        // TODO Auto-generated method stub
        int index = name.lastIndexOf('.');
        if(index == -1){ 
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
    
}

我们在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。

测试

现在我们要编写测试代码。我们知道如果调用一个Test对象的say方法,它会输出"Say Hello"这条字符串。但现在是我们把Test.class放置在应用工程所有的目录之外,我们需要加载它,然后执行它的方法。具体效果如何呢?我们编写的DiskClassLoader能不能顺利完成任务呢?我们拭目以待。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
    
        //创建自定义classloader对象。
        DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
        try {
            //加载class文件
            Class c = diskLoader.loadClass("com.frank.test.Test");
            
            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //通过反射调用Test类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException 
                        | NoSuchMethodException
                        | SecurityException | 
                        IllegalArgumentException | 
                        InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
    }

}

我们点击运行按钮,结果显示。

这里写图片描述

可以看到,Test类的say方法正确执行,也就是我们写的DiskClassLoader编写成功。

回首

讲了这么大的篇幅,自定义ClassLoader才姗姗来迟。 很多同学可能觉得前面有些啰嗦,但我按照自己的思路,我觉得还是有必要的。因为我是围绕一个关键字进行讲解的。

关键字是什么?

关键字 路径

  • 从开篇的环境变量
  • 到3个主要的JDK自带的类加载器
  • 到自定义的ClassLoader

它们的关联部分就是路径,也就是要加载的class或者是资源的路径。
BootStrap ClassLoader、ExtClassLoader、AppClassLoader都是加载指定路径下的jar包。如果我们要突破这种限制,实现自己某些特殊的需求,我们就得自定义ClassLoader,自已指定加载的路径,可以是磁盘、内存、网络或者其它。

所以,你说路径能不能成为它们的关键字?

当然上面的只是我个人的看法,可能不正确,但现阶段,这样有利于自己的学习理解。

自定义ClassLoader还能做什么?

突破了JDK系统内置加载路径的限制之后,我们就可以编写自定义ClassLoader,然后剩下的就叫给开发者你自己了。你可以按照自己的意愿进行业务的定制,将ClassLoader玩出花样来。

玩出花之Class解密类加载器

常见的用法是将Class文件按照某种加密手段进行加密,然后按照规则编写自定义的ClassLoader进行解密,这样我们就可以在程序中加载特定了类,并且这个类只能被我们自定义的加载器进行加载,提高了程序的安全性。

下面,我们编写代码。

1.定义加密解密协议

加密和解密的协议有很多种,具体怎么定看业务需要。在这里,为了便于演示,我简单地将加密解密定义为异或运算。当一个文件进行异或运算后,产生了加密文件,再进行一次异或后,就进行了解密。

2.编写加密工具类

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;


public class FileUtils {
    
    public static void test(String path){
        File file = new File(path);
        try {
            FileInputStream fis = new FileInputStream(file);
            FileOutputStream fos = new FileOutputStream(path+"en");
            int b = 0;
            int b1 = 0;
            try {
                while((b = fis.read()) != -1){
                    //每一个byte异或一个数字2
                    fos.write(b ^ 2);
                }
                fos.close();
                fis.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

我们再写测试代码

FileUtils.test("D:\\lib\\Test.class");

这里写图片描述
然后可以看见路径D:\\lib\\Test.class下Test.class生成了Test.classen文件。

编写自定义classloader,DeClassLoader

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


public class DeClassLoader extends ClassLoader {
    
    private String mLibPath;
    
    public DeClassLoader(String path) {
        // TODO Auto-generated constructor stub
        mLibPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // TODO Auto-generated method stub
        
        String fileName = getFileName(name);
        
        File file = new File(mLibPath,fileName);
        
        try {
            FileInputStream is = new FileInputStream(file);
            
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            byte b = 0;
            try {
                while ((len = is.read()) != -1) {
                    //将数据异或一个数字2进行解密
                    b = (byte) (len ^ 2);
                    bos.write(b);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            
            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            
            return defineClass(name,data,0,data.length);
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return super.findClass(name);
    }

    //获取要加载 的class文件名
    private String getFileName(String name) {
        // TODO Auto-generated method stub
        int index = name.lastIndexOf('.');
        if(index == -1){ 
            return name+".classen";
        }else{
            return name.substring(index+1)+".classen";
        }
    }
    
}

测试

我们可以在ClassLoaderTest.java中的main方法中如下编码:

DeClassLoader diskLoader = new DeClassLoader("D:\\lib");
        try {
            //加载class文件
            Class c = diskLoader.loadClass("com.frank.test.Test");
            
            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //通过反射调用Test类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException 
                        | NoSuchMethodException
                        | SecurityException | 
                        IllegalArgumentException | 
                        InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

查看运行结果是:

这里写图片描述

可以看到了,同样成功了。现在,我们有两个自定义的ClassLoader:DiskClassLoader和DeClassLoader,我们可以尝试一下,看看DiskClassLoader能不能加载Test.classen文件也就是Test.class加密后的文件。

我们首先移除D:\\lib\\Test.class文件,只剩下一下Test.classen文件,然后进行代码的测试。

DeClassLoader diskLoader1 = new DeClassLoader("D:\\lib");
        try {
            //加载class文件
            Class c = diskLoader1.loadClass("com.frank.test.Test");
            
            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //通过反射调用Test类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException 
                        | NoSuchMethodException
                        | SecurityException | 
                        IllegalArgumentException | 
                        InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
        try {
            //加载class文件
            Class c = diskLoader.loadClass("com.frank.test.Test");
            
            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //通过反射调用Test类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException 
                        | NoSuchMethodException
                        | SecurityException | 
                        IllegalArgumentException | 
                        InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
    }

运行结果:
这里写图片描述

我们可以看到。DeClassLoader运行正常,而DiskClassLoader却找不到Test.class的类,并且它也无法加载Test.classen文件。

Context ClassLoader 线程上下文类加载器

前面讲到过Bootstrap ClassLoader、ExtClassLoader、AppClassLoader,现在又出来这么一个类加载器,这是为什么?

前面三个之所以放在前面讲,是因为它们是真实存在的类,而且遵从”双亲委托“的机制。而ContextClassLoader其实只是一个概念。

查看Thread.java源码可以发现

public class Thread implements Runnable {

/* The context ClassLoader for this thread */
   private ClassLoader contextClassLoader;
   
   public void setContextClassLoader(ClassLoader cl) {
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           sm.checkPermission(new RuntimePermission("setContextClassLoader"));
       }
       contextClassLoader = cl;
   }

   public ClassLoader getContextClassLoader() {
       if (contextClassLoader == null)
           return null;
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                  Reflection.getCallerClass());
       }
       return contextClassLoader;
   }
}

contextClassLoader只是一个成员变量,通过setContextClassLoader()方法设置,通过getContextClassLoader()设置。

每个Thread都有一个相关联的ClassLoader,默认是AppClassLoader。并且子线程默认使用父线程的ClassLoader除非子线程特别设置。

我们同样可以编写代码来加深理解。
现在有2个SpeakTest.class文件,一个源码是

package com.frank.test;

public class SpeakTest implements ISpeak {

    @Override
    public void speak() {
        // TODO Auto-generated method stub
        System.out.println("Test");
    }

}

它生成的SpeakTest.class文件放置在D:\\lib\\test目录下。
另外ISpeak.java代码

 package com.frank.test;

public interface ISpeak {
    public void speak();

}

然后,我们在这里还实现了一个SpeakTest.java

package com.frank.test;

public class SpeakTest implements ISpeak {

    @Override
    public void speak() {
        // TODO Auto-generated method stub
        System.out.println("I\' frank");
    }

}

它生成的SpeakTest.class文件放置在D:\\lib目录下。

然后我们还要编写另外一个ClassLoader,DiskClassLoader1.java这个ClassLoader的代码和DiskClassLoader.java代码一致,我们要在DiskClassLoader1中加载位置于D:\\lib\\test中的SpeakTest.class文件。

测试代码:

DiskClassLoader1 diskLoader1 = new DiskClassLoader1("D:\\lib\\test");
Class cls1 = null;
try {
//加载class文件
 cls1 = diskLoader1.loadClass("com.frank.test.SpeakTest");
System.out.println(cls1.getClassLoader().toString());
if(cls1 != null){
    try {
        Object obj = cls1.newInstance();
        //SpeakTest1 speak = (SpeakTest1) obj;
        //speak.speak();
        Method method = cls1.getDeclaredMethod("speak",null);
        //通过反射调用Test类的speak方法
        method.invoke(obj, null);
    } catch (InstantiationException | IllegalAccessException 
            | NoSuchMethodException
            | SecurityException | 
            IllegalArgumentException | 
            InvocationTargetException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
    
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
new Thread(new Runnable() {
    
    @Override
    public void run() {
        System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
        
        // TODO Auto-generated method stub
        try {
            //加载class文件
        //    Thread.currentThread().setContextClassLoader(diskLoader);
            //Class c = diskLoader.loadClass("com.frank.test.SpeakTest");
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            Class c = cl.loadClass("com.frank.test.SpeakTest");
            // Class c = Class.forName("com.frank.test.SpeakTest");
            System.out.println(c.getClassLoader().toString());
            if(c != null){
                try {
                    Object obj = c.newInstance();
                    //SpeakTest1 speak = (SpeakTest1) obj;
                    //speak.speak();
                    Method method = c.getDeclaredMethod("speak",null);
                    //通过反射调用Test类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException 
                        | NoSuchMethodException
                        | SecurityException | 
                        IllegalArgumentException | 
                        InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}).start();

结果如下:
这里写图片描述

我们可以得到如下的信息:

  1. DiskClassLoader1加载成功了SpeakTest.class文件并执行成功。
  2. 子线程的ContextClassLoader是AppClassLoader。
  3. AppClassLoader加载不了父线程当中已经加载的SpeakTest.class内容。

我们修改一下代码,在子线程开头处加上这么一句内容。

Thread.currentThread().setContextClassLoader(diskLoader1);

结果如下:
这里写图片描述

可以看到子线程的ContextClassLoader变成了DiskClassLoader。

继续改动代码:

Thread.currentThread().setContextClassLoader(diskLoader);

结果:
这里写图片描述

可以看到DiskClassLoader1和DiskClassLoader分别加载了自己路径下的SpeakTest.class文件,并且它们的类名是一样的com.frank.test.SpeakTest,但是执行结果不一样,因为它们的实际内容不一样。

Context ClassLoader的运用时机

其实这个我也不是很清楚,我的主业是Android,研究ClassLoader也是为了更好的研究Android。网上的答案说是适应那些Web服务框架软件如Tomcat等。主要为了加载不同的APP,因为加载器不一样,同一份class文件加载后生成的类是不相等的。如果有同学想多了解更多的细节,请自行查阅相关资料。

总结

  1. ClassLoader用来加载class文件的。
  2. 系统内置的ClassLoader通过双亲委托来加载指定路径下的class和资源。
  3. 可以自定义ClassLoader一般覆盖findClass()方法。
  4. ContextClassLoader与线程相关,可以获取和设置,可以绕过双亲委托的机制。

下一步

  1. 你可以研究ClassLoader在Web容器内的应用了,如Tomcat。
  2. 可以尝试以这个为基础,继续学习Android中的ClassLoader机制。

引用

我这篇文章写了好几天,修修改改,然后加上自己的理解。参考了下面的这些网站。

  1. grepcode ClassLoader源码
  2. http://blog.csdn.net/xyang81/article/details/7292380
  3. http://blog.csdn.net/irelandken/article/details/7048817
  4. https://docs.oracle.com/javase/7/docs/api/java/net/URLClassLoader.html