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