工具类中注入的对象,在**static**(静态)方法中调用出现空指针的问题解决


工具类中注入的对象,在static(静态)方法中调用出现空指针的问题解决

场景一

自定义封装一个Redis方法的工具类——取名 RedisCacheUtil。最基础的调用,一般会先注入RedisTemplate对象,然后在具体方法中调用ValueOperations opsForValue = redisTemplate.getOpsForValue();,由此可以来调用Redis提供的方法操作Redis。由于每次在代码中想使用Redis的方法操作redis的数据时,会创建重复的代码(如下):

public class SpringbootXxlJobTestApplicationTests {
    @Resource
    private RedisTemplate redisTemplate;
    
    @Test
    void testGenerateId(){
        ValueOperations opsForValue = redisTemplate.getOpsForValue();
        Object mhd = opsForValue.get("mhd");
        System.out.println(mhd);
    }    
}

public class SpringbootXxlJobTestApplicationTests2 {
    @Resource
    private RedisTemplate redisTemplate;
    
    @Test
    void testGenerateId(){
        ValueOperations opsForValue = redisTemplate.getOpsForValue();
        Object mhd = opsForValue.get("mhd");
        System.out.println(mhd);
    }    
}

使用操作方法之前都得 redisTemplate.opsForValue(),封装之后,每次在其他代码方法里使用 Redis,就不用每次都注入对象RedisTemplate,只需要在RedisCacheUtil类中注入一次RedisTemplate,其他代码使用时注入 RedisCacheUtil即可:

@Component
public class RedisCacheUtil {
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * redis 设置 key-value 永不过期
     *
     * @param key
     * @param value
     */
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 指定缓存失效时间
     *
     * @param key      键
     * @param timeout  时间
     * @param timeUnit 时间单位
     * @return
     */
    public boolean expire(String key, long timeout, TimeUnit timeUnit) {
        return redisTemplate.expire(key, timeout, timeUnit);
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key
     * @return
     */
    public long getExpireTime(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 设置value的自增值
     * 给 key 的 value储存的数字加上指定的增量值,如果 key 不存在,则 key 的值会先被初始化为 0,然后再执行 INCR 操作
     *
     * @param key   键
     * @param delta 自增数值
     * @return 自增后的 value
     */
    public long  increment(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 设置 redis key-value 过期时间
     *
     * @param key
     * @param value
     * @param time
     * @param timeUnit
     */
    public void set(String key, Object value, long time, TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, time, timeUnit);
    }

    /**
     * 移除指定key 的过期时间
     *
     * @param key
     * @return
     */
    public boolean persist(String key) {
        return redisTemplate.boundValueOps(key).persist();
    }

}

一开始,封装的 redis 工具类中封装的方法采取的是 非静态方法,这时候在其他静态方法中调用 RedisCacheUtil中的非静态方法时,会出现 RedisCacheUtil 类调用出现空指针异常

错误代码示例: redisCacheUtil会空指针

@Slf4j
public class IdGeneratorUtil {
    @Resource
    private RedisCacheUtil redisCacheUtil;
    
    public static String generateId() {
        ID_PREFIX = "ID";

        String datetime = "";
        try {
            ....
            Long autoID = RedisCacheUtil.increment(key, 1);
            // 如果 autoID = 1,则 key不存在,给 key设置过期时间
            if (autoID == 1) {
                // 设置key 1小时过期
                RedisCacheUtil.expire(key, REDIS_TIMEOUT, TimeUnit.SECONDS);
            }
            // 获取订单号,时间戳 + 唯一自增Id(最少4位数,不够前方补0)
            return datetime + String.format("%04d", autoID);
        } catch (Exception e) {
            log.error("Redis is abnormal, the orderNo will be generated with ObjectId.");
            //12位时间戳     datetime = datetime + ObjectId.get();
            return datetime;
        }
    }
}

以上代码即使使用 @Resource注入 RedisCacheUtil注入也会失败,在静态方法中调用也会导致空指针异常。

即使使用 static 修饰也不行,private static RedisCacheUtil redisCacheUtil;

然后,将 RedisCacheUtil 类中的非静态方法改成静态方法,加上 static 也不行。最后采取以下方式解决,注意:

  1. 要在工具类上加上 @Component 注解。
  2. 在工具类定义两个方法:
    • 一个用 @Resource注入 RedisTemplate对象,一个定义静态的成员变量 static RedisTemplate redisTempStatic
  3. 定义一个方法,并用 @PostConstruct 注解修饰,方法中将注入的非静态实例对象 redisTemplate赋值给静态的成员变量 redisTempStatic
@Component
public class RedisCacheUtil {
    @Resource
    private RedisTemplate redisTemplate;

    private static RedisTemplate redisTempStatic;

    @PostConstruct
    public void setRedisTemplate() {
        redisTempStatic = this.redisTemplate;
    }

    /**
     * redis 设置 key-value 永不过期
     *
     * @param key
     * @param value
     */
    public static void set(String key, Object value) {
        redisTempStatic.opsForValue().set(key, value);
    }

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public static Object get(String key) {
        return key == null ? null : redisTempStatic.opsForValue().get(key);
    }

    /**
     * 指定缓存失效时间
     *
     * @param key      键
     * @param timeout  时间
     * @param timeUnit 时间单位
     * @return
     */
    public static boolean expire(String key, long timeout, TimeUnit timeUnit) {
        return redisTempStatic.expire(key, timeout, timeUnit);
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key
     * @return
     */
    public static long getExpireTime(String key) {
        return redisTempStatic.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 设置value的自增值
     * 给 key 的 value储存的数字加上指定的增量值,如果 key 不存在,则 key 的值会先被初始化为 0,然后再执行 INCR 操作
     *
     * @param key   键
     * @param delta 自增数值
     * @return 自增后的 value
     */
    public static long  increment(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTempStatic.opsForValue().increment(key, delta);
    }

    /**
     * 设置 redis key-value 过期时间
     *
     * @param key
     * @param value
     * @param time
     * @param timeUnit
     */
    public static void set(String key, Object value, long time, TimeUnit timeUnit) {
        redisTempStatic.opsForValue().set(key, value, time, timeUnit);
    }

    /**
     * 移除指定key 的过期时间
     *
     * @param key
     * @return
     */
    public static boolean persist(String key) {
        return redisTempStatic.boundValueOps(key).persist();
    }

}
@Slf4j
public class IdGeneratorUtil {
    
    public static String generateId() {
        ID_PREFIX = "ID";

        String datetime = "";
        try {
            ....
            Long autoID = RedisCacheUtil.increment(key, 1);
            // 如果 autoID = 1,则 key不存在,给 key设置过期时间
            if (autoID == 1) {
                // 设置key 1小时过期
                RedisCacheUtil.expire(key, REDIS_TIMEOUT, TimeUnit.SECONDS);
            }
            // 获取订单号,时间戳 + 唯一自增Id(最少4位数,不够前方补0)
            return datetime + String.format("%04d", autoID);
        } catch (Exception e) {
            log.error("Redis is abnormal, the orderNo will be generated with ObjectId.");
            //12位时间戳     datetime = datetime + ObjectId.get();
            return datetime;
        }
    }
}

以上代码,可实现在其他类的静态方法中通过 RedisCacheUtil 直接调用当中的方法。


如何在工具类中的静态方法使用 Mapper 类的方法?

一般思路是在类中 注入 Mapper 实例对象,然后在静态方法中通过实例对象调用方法。但不幸的是,这是不行的,会报空指针

错误示例:

@Slf4j
public class IdGeneratorUtil {
	@Resource
	private static IdGenerateMapper idGenerateMapper;
    
    public static String generateId() {
        ID_PREFIX = "ID";

        String datetime = "";
        datetime = idGenerateMapper.getCurrentDateTime();
       
    }
}

在静态方法中使用Mapper,可以先在 static 代码块中将要注入的mapper实例对象赋值,通过 Spring 应用上下文中获取 Bean 实例和其他资源。

修改后的代码:

@Slf4j
public class IdGeneratorUtil {
	private static IdGenerateMapper idGenerateMapper;
	
	static {
        idGenerateMapper = SpringUtils.getBeanByClass(IdGenerateMapper.class);
        try {
            // redis当前最大时间 key
            REDIS_TIME_MAX_KEY = "mhd";
        } catch (Exception e) {
            log.error("Failed to create a jedisTemplate.");
        }
    }
    
    public static String generateId() {
        ID_PREFIX = "ID";

        String datetime = "";
        datetime = idGenerateMapper.getCurrentDateTime();
       
    }
}
package com.example.springbootxxljobtest.utils;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

/**
 * @Description:
 * @title: SpringContextUtils
 * @Author mhd
 * @Date: 2024/9/23 9:45
 * @Version 1.0
 */
@Component
public final class SpringContextUtils implements DisposableBean, ApplicationContextAware {
    private static ApplicationContext applicationContext;

    public SpringContextUtils() {
    }

    @Override
    public void destroy() throws Exception {
        clearContext();
    }

    private static void clearContext() {
        applicationContext = null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> tClass) {
        return applicationContext.getBean(tClass);
    }

    public static String getProperty(String property) {
        Environment var1 = (Environment) getBean(Environment.class);
        return var1.getProperty(property);
    }

    public static <T> T getProperty(String property, Class<T> tClass) {
        Environment var2 = (Environment) getBean(Environment.class);
        return var2.getProperty(property,tClass);
    }

}

SpringUtilsSpringContextUtils 工具类,二选一即可。

注意:

自己封装定义的这两个工具类的名称涉及到Spring框架中的关键字,在取名的时候建议避免和Spring的名字区分开,避免类名冲突。比如 getSpringUtils ,虽然不咋优雅,但能避坑。

package com.example.springbootxxljobtest.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class SpringUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtils.applicationContext = applicationContext;
    }

    /**
     * 获取applicationContext对象
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static <T> T getBean(String id, Class<T> clazz) {
        return (T) getBeanById(id);
    }
    
	public static <T> T getBean(String id) {
        return (T) getBeanById(id);
    }

    /**
     * 根据bean的id来查找对象
     *
     * @param id
     * @return
     */
    public static Object getBeanById(String id) {
        return applicationContext.getBean(id);
    }

    /**
     * 根据bean的class来查找对象
     *
     * @param c
     * @return
     */
    public static <T> T getBeanByClass(Class<T> c) {
        return applicationContext.getBean(c);
    }

    /**
     * 根据bean的class来查找所有的对象(包括子类)
     *
     * @param c
     * @return
     */
    public static <T> Map<String, T> getBeansByClass(Class<T> c) {
        return applicationContext.getBeansOfType(c);
    }
}

SpringBoot
Maven
JAVA-技能点