Java架构师成长之路-代码优化方案

1.Enum代码优化

一般情况下我们写枚举类时可能需要根据枚举类中的某个属性获取对应的枚举对象或者属性,例如:

public enum Enum {
    /**
     * test
     */
    A(1, "1desc"), B(2, "2desc"), C(3, "3desc");

    private int value;
    private String desc;

    Enum(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }

    public int getValue() {
        return value;
    }

    public String getDesc() {
        return desc;
    }

    /**
     * 根据value获取desc
     *
     * @param value value
     * @return desc
     */
    public static String getDescByValue(@NonNull Integer value) {
        return Optional.ofNullable(getByValue(value)).map(Enum::getDesc).orElse(null);
    }

    /**
     * 根据value获取当前枚举对象
     *
     * @param value value
     * @return this
     */
    public static Enum getByValue(@NonNull Integer value) {
        Objects.requireNonNull(value);
        for (Enum anEnum : values()) {
            if (Objects.equals(anEnum.value, value)) {
                return anEnum;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        System.out.println(Enum.getDescByValue(2));
    }

}

但是此种方式如果对于枚举类中枚举比较多的时候,每次根据value获取desc都会去循环遍历获取到想要的值,这种方式非常的不好。接下来推荐一种方案,是一种典型的以空间换时间的思想,我们对枚举类中方法进行了一次改造,代码如下:

    public static Map<Integer, Enum> maps = new HashMap<>();

    // 类加载时把此枚举类中的属性做个映射存入map中
    static {
        for (Enum anEnum : values()) {
            maps.put(anEnum.value, anEnum);
        }
    }

    /**
     * 从map中获取,事实证明map中通过map中get方法hash计算来定位目标,比通过循环要快的过
     *
     * @param value value
     * @return this
     */
    public static Enum getByValue(@NonNull Integer value) {
        Objects.requireNonNull(value);
        return maps.get(value);
    }

如果枚举只有几个,还是不太建议用以上方式,可能并不能带来很好的性能提升,不如for更快捷。对于枚举固定的场景,不经常增加或者减少枚举时我们可以使用以下方式,性能最佳,代码如下:

    public static Enum getByValue(@NonNull Integer value) {
        Objects.requireNonNull(value);
        switch (value) {
            case 1:
                return A;
            case 2:
                return B;
            case 3:
                return C;
            default:
                throw new IllegalStateException("Unexpected value: " + value);
        }
    }

以上几种方式可以看场景选择

2.Bean属性复制优化方案

一般情况下,我们对于一些Class不同的时候,需要做一个类型转换,通常情况我们会使用如下方式:

public class TestCopy {

    @Data
    public static class User {
        private Integer id;
        private String name;
        private String password;
    }

    @Data
    public static class UserVo {
        private Integer id;
        private String name;
    }

    public static void main(String[] args) {
        User user = new User();
        user.setId(1);
        user.setName("aaa");
        user.setPassword("123456");

        UserVo userVo = new UserVo();
        userVo.setId(user.getId());
        userVo.setName(user.getName());
        System.out.println(userVo);
    }
}    

但是对于属性值比较多的时候,这个工作量就会有点大了,还可以有一种方式,通过反射进行相应的复制操作,我们可以采用如下方式:

    public static void main(String[] args) {
        User user = new User();
        user.setId(1);
        user.setName("aaa");
        user.setPassword("123456");

        UserVo userVo = new UserVo();
        BeanUtils.copyProperties(user, userVo);
        System.out.println(userVo);
    }

但是这一种在运行时反射进行赋值操作,难免会有一些性能消耗,我们可以通过mapstruct来实现复制,它通过在编译时来帮我们生成代码,来实现复制,使用案例如下:

    public static void main(String[] args) {
        User user = new User();
        user.setId(1);
        user.setName("aaa");
        user.setPassword("123456");

        UserVo userVo = Convert.INSTANCE.convert(user);
        System.out.println(userVo);
    }

    @Mapper
    public interface Convert {
    
        Convert INSTANCE = Mappers.getMapper(Convert.class);

        /**
         * User to UserVo
         *
         * @param user User
         * @return UserVo
         */
        UserVo convert(User user);
        
        // ...以及其他转换需求
        
    }

mapstruct更多使用方式可以查看官网文档:mapstructopen in new window

3.Map的getOrDefault使用细节优化

在面对一些使用到getOrDefault业务场景时,一般情况下我们使用getOrDefault如下:

    public static void main(String[] args) {
        Map<Object, Object> hashMap = new HashMap<>();
        User user = new User();
        user.setName("abc");
        hashMap.put("key", user);
        
        User defaultUser = new User();
        defaultUser.setName("bbb");
        Object value = hashMap.getOrDefault("key", defaultUser);
        System.out.println(value);
    }

但是这样其实写有一个小细节问题,就是即使这个key在map中存在或者不存在,getOrDefault("key", new User())里面的new User()都会被执行,如果执行的是一个复杂的业务,造成多余的性能开销,我们可以通过简单的if判断来代替这种方式,或者也可以修改为如下代码:

		
        Object value = Optional.ofNullable(hashMap.get("key")).orElseGet(() -> {
        	// 如果存在的话,这里面代码不会执行
            User defaultUser = new User();
            defaultUser.setName("bbb");
            return defaultUser;
        });
        System.out.println(value);

其实map中也提供了一个类似的方法computeIfAbsent,不过此方法会把计算的结果存入到map中。

4.循环查询数据库优化

在有些场景下,我们有时不得已需要循环查询数据库来关联相应的数据,例如:

@lombok.Data
public class Data {
    private Integer id;
    private String dataName;
    private Integer userId;
}
@Data
public class DataVo {
    private Integer id;
    private String dataName;

    private Integer userId;
    private String userName;
}

public class ServiceTest {

    @Resource
    private DataFeign dataFeign;
    @Resource
    private UserMapper userMapper;

    public List<DataVo> listDataVo(String name) {
        // 例如根据名称模糊查询到一批与用户id进行关联数据,没有关联用户name
        List<Data> datas = this.dataFeign.list(name);
        // 但是我们可能会封装一层vo类,可能会涉及到一些别数据,例如需要查询到这个用户的名称
        return datas.stream().map(m -> {
            DataVo dataVo = new DataVo();
            dataVo.setId(m.getId());
            dataVo.setDataName(m.getDataName());
            dataVo.setUserId(m.getUserId());
            // 循环查询 关联用户name
            User userById = this.userMapper.getUserById(m.getUserId());
            dataVo.setUserName(userById.getName());
            return dataVo;
        }).collect(Collectors.toList());
    }

}

以上这种方式极其不好,如果数据量过大,循环查询n次数据库性能消耗极大,因此推荐如下方法:

    public List<DataVo> listDataVo(String name) {
        // 例如根据名称模糊查询到一批与用户id进行关联数据,没有关联用户name
        List<Data> datas = this.dataFeign.list(name);
        // 先查询到这个List<Data>用到的有所用户
        Set<Integer> userIds = datas.stream().map(Data::getUserId).collect(Collectors.toSet());
        // 根据用户id批量查询 实际只需要查询使用到的字段
        List<User> users = this.userMapper.getUserByIds(userIds);
        // 做有关id与username的映射关系
        Map<Integer, String> maps = users.stream().collect(Collectors.toMap(User::getId, User::getName));
        // 处理数据
        return datas.stream().map(m -> {
            DataVo dataVo = new DataVo();
            dataVo.setId(m.getId());
            dataVo.setDataName(m.getDataName());
            dataVo.setUserId(m.getUserId());
            // 关联用户name
            String username = maps.get(m.getUserId());
            dataVo.setUserName(username);
            return dataVo;
        }).collect(Collectors.toList());
    }

这样就只操作了两次数据库而达成目的,可能场景举例的不太好,其实主要的是学习这种思想。

5.ThreadLocal使用优化

在一些场景中我们可能需要使用ThreadLocal,例如我们在权限拦截器通过之后可以把当前登录用户的信息保存到ThreadLocal中,提供给后面链路使用,但是此种方式并不能把存入的数据传递给子线程中,因此我们可以使用InheritableThreadLocal来解决:

ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

但是此种方式虽然能解决父子线程数据传递问题,但是如果我们使用线程池,依然会出现数据错误问题,此时我们可以通过使用TransmittableThreadLocal:

ThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();

TransmittableThreadLocal是Alibaba开源的用于解决在使用线程池等会缓存线程的组件情况下传递ThreadLocal问题的InheritableThreadLocal扩展。具体使用方式可以参考:TransmittableThreadLocalopen in new window

6.缓存使用细节优化(主要应对极端性能场景)

只有当从数据库查询的数据或者调用其它服务接口获取的数据有很多缓存数据Key重复的场景使用,否则达不到优化的目的。

    @Test
    public List<DataVo> listDataVo() {
        List<Data> datas = this.dataService.list();
        List<DataVo> dataVos = new ArrayList<>();
        // 创建一个本地缓存
        Map<String, String> localCache = new HashMap<>();
        for (Data data : datas) {
            String cacheKey = "prefix:" + data.getId();
            // 根绝编号等等 查询缓存中的数据 组装数据
            String name = localCache.computeIfAbsent(cacheKey, k -> {
                // 如果localCache不存在时 再去redis查询。  实际使用中不只是Redis能用到,如果关联一些数据库表,也可以。看此处具体实现
                return redisTemplate.opsForValue().get(k);
            });
            DataVo dataVo = new DataVo();
            dataVo.setId(data.getId());
            dataVo.setName(name);
            dataVos.add(dataVo);
        }
        return dataVos;
    }

以上只是解决类似问题的思路,项目中使用需要看具体场景

7.单例使用优化(单例工具)

在一些场景中,我们经常会用到单例模式,但是对于使用一些懒汉双检锁或者饿汉式单例模式都不算太好,懒汉模式每次都要多走几步判断以及加锁实现,性能不算太好。饿汉式类加载时就初始化,浪费内存。接下来给大家推荐一个适配jdk1.8以上的单例工具使用:

public final class Singleton<R> implements Supplier<R> {

    private boolean initialized = false;
    private volatile Supplier<R> instanceSupplier;

    public static <R> Singleton<R> from(final Supplier<R> original) {
        return new Singleton<>(original);
    }

    public static <T, R> Singleton<R> from(final Function<T, R> original, T arg0) {
        return fromLazy(original, () -> arg0);
    }

    public static <T, U, R> Singleton<R> from(final BiFunction<T, U, R> original, T arg0, U arg1) {
        return fromLazy(original, () -> arg0, () -> arg1);
    }

    public static <T, R> Singleton<R> fromLazy(final Function<T, R> original, Supplier<T> arg0Supplier) {
        return from(() -> original.apply(arg0Supplier.get()));
    }

    public static <T, U, R> Singleton<R> fromLazy(final BiFunction<T, U, R> original, Supplier<T> arg0Supplier, Supplier<U> arg1Supplier) {
        return from(() -> original.apply(arg0Supplier.get(), arg1Supplier.get()));
    }

    public Singleton(final Supplier<R> original) {
        instanceSupplier = () -> {
            synchronized (original) {
                if (!initialized) {
                    final R singletonInstance = original.get();
                    instanceSupplier = () -> singletonInstance;
                    initialized = true;
                }
                return instanceSupplier.get();
            }
        };
    }

    @Override
    public R get() {
        return instanceSupplier.get();
    }
}

使用细节可以自己摸索下。

8.@Value("${...}")使用细节

例如:有的时候我们在application.yml配置了一个url,需要很多地方都要用到,例如application.yml配置如下:

	server.urls: http://...

使用如下:

	// a service配置了
    @Value("${server.url}")
    public String serverUrl;
	// b service配置了
    @Value("${server.url}")
    public String serverUrl;
    // c ....

此时我们完全可以自定义一个注解来代替以上用法,这样也方便管理与修改,例如:

@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Value("${server.url}")
public @interface ServerUrl {

}

使用方法如下:

	// a service配置了
    @ServerUrl
    public String serverUrl;
	// b service配置了
    @ServerUrl
    public String serverUrl;
    // c ....
更新日期:
作者: 丁乾文, qwding