JAVA April 01, 2021

Java单元测试动态修改环境变量

Words count 8.3k Reading time 8 mins. Read count 0

今天再写单元测试的时候,遇到一个比较有趣的事情,程序需要读取一个环境变量,而这个变量又是动态生成的,所以在执行单元测试之前要进行环境变量的配置。目前总结了两种方案可以实现动态修改环境变量,修改后的环境变量仅对当前进程生效,即仅在当前Java进程中调用System.getenv(name)生效,分别是通过反射修改Runtime中保存环境变量的Map、和通过JNI的方式调用系统的setenv方法。下面将分别对两种方案进行实现。

1 通过反射修改Runtime中保存环境变量的Map

参考:https://stackoverflow.com/questions/580085/is-it-possible-to-set-an-environment-variable-at-runtime-from-java

在Java中获取环境变量,可以使用如下两种方法:

public static String getenv(String name);
public static java.util.Map<String,String> getenv();

getenv()返回的是一个Map,但是这个map是不可修改的,如果我们对其修改会报如下错误:

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Collections.java:1459)
    at learn.examples.jni.EnvNativeUtilsTest.main(EnvNativeUtilsTest.java:8)

image-20210401182852336

这种方案修改环境变量的原理很简单,我们可以通过反射的方式拿到这个Map,并对其修改即可。直接上代码:

package learn.examples.jni;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author shirukai
 */
public class EnvUtils {
    public static void setEnv(String name, String value) throws Exception {
        getModifiableEnvironment().put(name, value);
    }

    /**
     * 通过反射的方式从Runtime中获取存储环境变量的Map,返回的Map是可变的
     * Copy from https://stackoverflow.com/questions/580085/is-it-possible-to-set-an-environment-variable-at-runtime-from-java
     *
     * @return Map<String, String></String,String>
     * @throws Exception e
     */
    @SuppressWarnings("unchecked")
    private static Map<String, String> getModifiableEnvironment() throws Exception {
        Class<?> pe = Class.forName("java.lang.ProcessEnvironment");
        Method getenv = pe.getDeclaredMethod("getenv");
        getenv.setAccessible(true);
        Object unmodifiableEnvironment = getenv.invoke(null);
        Class<?> map = Class.forName("java.util.Collections$UnmodifiableMap");
        Field m = map.getDeclaredField("m");
        m.setAccessible(true);
        return (Map<String, String>) m.get(unmodifiableEnvironment);
    }

    public static void main(String[] args) throws Exception {
        EnvUtils.setEnv("TEST_SET_ENV","test-set-env");
        System.out.println(System.getenv("TEST_SET_ENV"));
    }
}

2 通过JNI的方式调用系统的setenv方法

2.1 基本概念

JNI是Java Native Interface的缩写,通过使用Java本地接口书写程序,可以确保代码在不同的平台上方便移植。通俗的说,我们可以使用Java调用C/C++代码。关于JNI的知识可以参考:https://blog.csdn.net/xyang81/article/details/41777471这篇文章。

2.2 setenv方法

有了JNI,我们就可以很方便的调用标准库中的setenv方法,首先我们可以在Linux的环境中,输入man setenv

image-20210401184023917

从说明文档可以看出,要使用setenv方法首先要引入标准库,然后调用该方法,当前方法参数也非常简单name为变量名称,value为变量值,overwrite为是否覆盖的标志位,如果为0不覆盖,非零将进行覆盖。

2.3 使用JNI调用setenv方法

JNI开发的基本流程如下(参考这篇文章):

1、编写声明了native方法的Java类
2、将Java源代码编译成class字节码文件
3、用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni参数表示将class中用native声明的函数生成jni规则的函数)
4、用本地代码实现.h头文件中的函数
5、将本地代码编译成动态库(windows:*.dll,linux/unix:*.so,mac os x:*.jnilib)
6、拷贝动态库至 java.library.path 本地库搜索目录下,并运行Java程序

下面就将一步一步实现,笔者电脑为Mac,仅记录在Mac下的实现,其它平台实现参考具体资料。

2.3.1 编写声明了native方法的Java类

首先我们创建一个名为EnvNativeUtils的类:

public class EnvNativeUtils {
    public static native void setEnv(String name, String value);
}

2.3.2 编译

编译很简单,我们可以直接使用Maven的compile直接编译,这时会在target/classes下生成对应的class文件,这里不做演示。

2.3.3 用javah -jni命令生成.h头文件

使用如下命令生成.h头文件:

javah -classpath /Users/shirukai/hollysys/repository/learn-demo-java/examples/target/classes/ learn.examples.jni.EnvNativeUtils

-classpath 指定编译后的类路径,这里笔者指定的是绝对路径,后面的参数是包路径。

执行命令后,会在当前文件夹下生成一个名为learn_examples_jni_EnvNativeUtils.h的头文件,内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class learn_examples_jni_EnvNativeUtils */

#ifndef _Included_learn_examples_jni_EnvNativeUtils
#define _Included_learn_examples_jni_EnvNativeUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     learn_examples_jni_EnvNativeUtils
 * Method:    setEnv
 * Signature: (Ljava/lang/String;Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_learn_examples_jni_EnvNativeUtils_setEnv
  (JNIEnv *, jclass, jstring, jstring);

#ifdef __cplusplus
}
#endif
#endif

2.3.4 实现.h头文件中的函数

在当前文件夹下新建一个名为EnvNativeUtils.c的文件,内容如下:

#include "learn_examples_jni_EnvNativeUtils.h"
#include <stdlib.h>
#ifdef __cplusplus
extern "C"
{
#endif

/*
 * Class:     com_study_jnilearn_HelloWorld
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT void JNICALL Java_learn_examples_jni_EnvNativeUtils_setEnv
  (JNIEnv *env, jclass cls, jstring j_name, jstring j_value)
{
    const char *name = NULL;
    const char *value = NULL;
    char buff[128] = { 0 };
    name = (*env)->GetStringUTFChars(env, j_name, NULL);
    value = (*env)->GetStringUTFChars(env, j_value, NULL);
    setenv(name,value,0);
}
#ifdef __cplusplus
}

#endif

2.3.5 将本地代码编译成动态库

使用如下命令编译成动态库

gcc -dynamiclib -o /Users/shirukai/hollysys/repository/learn-demo-java/examples/src/main/java/learn/examples/jni/libenvnativeutils.jnilib /Users/shirukai/hollysys/repository/learn-demo-java/examples/src/main/java/learn/examples/jni/EnvNativeUtils.c -I/$JAVA_HOME/include -I/$JAVA_HOME/include/darwin

2.3.6 代码中使用绝对路径加载动态库

执行上面命令之后,会在对应的目录生成一个libenvnativeutils.jnilib的动态库,我们在代码里可以直接指定动态库的路径对其进行加载

public class EnvNativeUtils {
    public static native void setEnv(String name, String value);

    static {
        System.load("/Users/shirukai/hollysys/repository/learn-demo-java/examples/src/main/java/learn/examples/jni/libenvnativeutils.jnilib");
    }
}

2.3.7 验证

上述操作完成之后,我们就可以进行验证了,编写测试类EnvNativeUtilsTest:

public class EnvNativeUtilsTest {
    public static void main(String[] args) {
        EnvNativeUtils.setEnv("TEST_ENV_BY_JNI","Hello JNI");
        System.out.println(System.getenv("TEST_ENV_BY_JNI"));
    }
}

执行main方法,环境变量生效。

0%