前言
有时候是不是很苦恼想在不修改别人的应用(或者统一处理所有的应用)情况下如何添加额外功能?那么-javaagent启动参数就能处理这个问题。
一、java代理的两种实现方式
1、premain
以java参数-javaagent添加代理包方式实现,在main方法执行前处理业务逻辑。
public static void premain(String agrentsArgs, Instrumentation instrumentation){}
或者 public static void premain(String agrentsArgs){}
2、agentmain
以VirtualMachine远程添加代理包,agentmain是在main方法执行后,同时可以执行多次(这种方式可以实现热加载,达到不重启更新功能效果)
public static void agentmain(String agrentsArgs, Instrumentation instrumentation){}
或者 public static void agentmain(String agrentsArgs){}
嗯,具体意义这里就不讲了,主要实践,它能什么。
二、premain里面注册MBean,获取应用端口
本章主要讲用premain添加代理包实现业务逻辑
准备工作
编写一个IpPortPremainAgent类,里面用的是premain
package agent;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;
import agent.listenersupport.ListenerSupport;
import agent.mxbean.ServerIpPortObject;
import agent.register.RegisterServer;
import agent.util.FileUtil;
import agent.util.StringUtil;
* 描述:代理类(premain 方式修改FileEncodingApplicationListener)
* @author sakyoka
* @date 2022年9月4日 上午11:22:09
public class IpPortPremainAgent {
/** springboot的监听类 class文件名字包含.class */
private static final String FILE_ENCODING_APPLICATION_LISTENER_CLASS = "FileEncodingApplicationListener.class";
/** springboot的监听类 类路径*/
private static final String FILE_ENCODING_APPLICATION_LISTENER_PACKAGE = "org/springframework/boot/context/FileEncodingApplicationListener";
/** listener 支撑包名字包含.jar*/
private static final String LISTENER_JAR= "springboot-listener-support.jar";
/** 代理包名字包含.jar*/
private static final String PREMAIN_JAR = "springboot-agent-premain.jar";
/** 缓存文件夹名字*/
private static final String CACHE_FOLDER = "cache";
* 描述:main方法前
* @author sakyoka
* @date 2022年9月4日 上午11:22:24
* @param agrentsArgs
* @param instrumentation
public static void premain(String agrentsArgs, Instrumentation instrumentation) {
System.out.println("获取到的参数:" + agrentsArgs);
if (!StringUtil.isBlank(agrentsArgs)){
System.out.println("选择直接执行注册对象...");
System.out.println("开始注册对象...");
ServerIpPortObject serverIpPortObject = RegisterServer.registerServerIpPortObject(null, agrentsArgs);
String ip = serverIpPortObject.getIp();
System.out.println(String.format("注册对象完毕。ip:%s,端口:%s", ip, agrentsArgs));
}else{
System.out.println("选择修改监听器形式注册对象...");
loadFileEncodingApplicationListenerSupportJar(instrumentation);
replaceFileEncodingApplicationListenerFile(instrumentation);
System.out.println("修改监听器完毕。");
* 描述:加载FileEncodingApplicationListener所需要的支撑包
* @author sakyoka
* @date 2022年9月4日 上午11:22:40
* @param instrumentation
private static void loadFileEncodingApplicationListenerSupportJar(Instrumentation instrumentation) {
System.out.println("开始加载jar包...");
String tempFilePath = generateTempSpringListenerSupportJarAbsolutePath();
System.out.println("缓存文件路径:" + tempFilePath);
File tempFile = new File(tempFilePath);
try (InputStream inputStream = ListenerSupport.getListenerSupportInputStream(LISTENER_JAR)){
if (!tempFile.exists()) {
//这里存在多应用执行问题,但是目前jar启动是顺序执行的
FileUtil.write(inputStream, tempFile);
System.out.println("缓存文件已生成");
}else {
System.out.println("缓存文件已存在,不需要再产生。");
System.out.println("开始添加jar包.");
instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(tempFile));
} catch (IOException e) {
throw new RuntimeException("加载jar包失败, jarPath:" + LISTENER_JAR, e);
System.out.println("加载jar包结束。");
* 描述:生成SpringListenerSupport缓存文件绝对路径
* @author sakyoka
* @date 2022年9月4日 上午11:22:51
* @return
private static String generateTempSpringListenerSupportJarAbsolutePath() {
String agentInnerPath = ListenerSupport.PREMAIN_JAR_ABSOLUTE;
String folder = agentInnerPath.substring(0, agentInnerPath.lastIndexOf(PREMAIN_JAR))
+ File.separator + CACHE_FOLDER ;
FileUtil.folderCreateIfNotExists(folder);
String tempFilePath = folder + File.separator + LISTENER_JAR;
return tempFilePath;
* 描述:替换FileEncodingApplicationListener
* @author sakyoka
* @date 2022年9月4日 上午11:23:14
* @param instrumentation
private static void replaceFileEncodingApplicationListenerFile(Instrumentation instrumentation) {
System.out.println("开始替换class...");
instrumentation.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
if (FILE_ENCODING_APPLICATION_LISTENER_PACKAGE.equals(className)){
return FileUtil.streamToByteArray(ListenerSupport
.getListenerSupportInputStream(FILE_ENCODING_APPLICATION_LISTENER_CLASS));
return classfileBuffer;
System.out.println("替换class完毕。");
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
package org.springframework.boot.context;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.StringUtils;
import listener.ServerIpPortObject;
* An {@link ApplicationListener} that halts application startup if the system file
* encoding does not match an expected value set in the environment. By default has no
* effect, but if you set {@code spring.mandatory_file_encoding} (or some camelCase or
* UPPERCASE variant of that) to the name of a character encoding (e.g. "UTF-8") then this
* initializer throws an exception when the {@code file.encoding} System property does not
* equal it.
* The System property {@code file.encoding} is normally set by the JVM in response to the
* {@code LANG} or {@code LC_ALL} environment variables. It is used (along with other
* platform-dependent variables keyed off those environment variables) to encode JVM
* arguments as well as file names and paths. In most cases you can override the file
* encoding System property on the command line (with standard JVM features), but also
* consider setting the {@code LANG} environment variable to an explicit
* character-encoding value (e.g. "en_GB.UTF-8").
* @author Dave Syer
public class FileEncodingApplicationListener
implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
private static final Log logger = LogFactory
.getLog(FileEncodingApplicationListener.class);
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
try {
String ip = this.getIp();
this.registerServerIpPortObject(ip, environment);
} catch (Exception e) {
logger.error("注册对象失败", e);
//environment.containsProperty("mandatoryFileEncoding")
if (!environment.containsProperty("spring.mandatory-file-encoding")
&& !environment.containsProperty("mandatoryFileEncoding")) {
return;
String encoding = System.getProperty("file.encoding");
String desired = environment.getProperty("spring.mandatory-file-encoding");
desired = StringUtils.isEmpty(desired) ? environment.getProperty("mandatoryFileEncoding") : desired;
if (encoding != null && !desired.equalsIgnoreCase(encoding)) {
logger.error("System property 'file.encoding' is currently '" + encoding
+ "'. It should be '" + desired
+ "' (as defined in 'spring.mandatoryFileEncoding').");
logger.error("Environment variable LANG is '" + System.getenv("LANG")
+ "'. You could use a locale setting that matches encoding='"
+ desired + "'.");
logger.error("Environment variable LC_ALL is '" + System.getenv("LC_ALL")
+ "'. You could use a locale setting that matches encoding='"
+ desired + "'.");
throw new IllegalStateException(
"The Java Virtual Machine has not been configured to use the "
+ "desired default character encoding (" + desired + ").");
* 描述:注册ServerIpPortObject
* @author sakyoka
* @date 2022年9月4日 上午11:20:05
* @param ip
* @param environment
private void registerServerIpPortObject(String ip, ConfigurableEnvironment environment) {
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
String port = environment.getProperty("server.port");
ServerIpPortObject serverIpPortObject = new ServerIpPortObject(ip, port);
String applicationName = environment.getProperty("spring.application.name");
if (port == null){
System.out.println("获取端口号失败,本次不进行注册对象.");
return ;
System.out.println(String.format("获取端口号成功,port:%s,开始进行注册对象", port));
try {
System.out.println(String.format("%s:开始注册ServerIpPortObject...", applicationName));
mBeanServer.registerMBean(serverIpPortObject, new ObjectName("com.sakyoka.test.agent:type=ServerIpPortObject"));
System.out.println(String.format("%s:注册ServerIpPortObject完毕。", applicationName));
} catch (Exception e) {
throw new RuntimeException(String.format("%s:注册失败ServerIpPortObject失败", applicationName), e);
* 描述:获取ip
* @author sakyoka
* @date 2022年9月4日 上午11:20:18
* @return ip
private String getIp(){
try {
InetAddress inetAddress = InetAddress.getLocalHost();
return inetAddress.getHostAddress();
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
ok,处理过程就这么多,现在看下启动jar包时候把代理包添加进去效果。
三、测试代理包
把代理包放到想要的位置,然后用-javaagent添加
带9999端口参数的(9999是随便给的值,这里不是真实端口)
-javaagent:D:/springboot-agent-premain.jar=9999
启动命令:
java -javaagent:D:/springboot-agent-premain.jar=9999 -Dfile.encoding=utf-8 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote= -Dcom.sun.management.jmxremote.port=12582 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar D:\jarmanage\jarManage\jarStroage\8f34f6bbde5f40bbb375b5d35f8fcf0c\jar-manage\jar-manage.jar >> D:\jarmanage\jarManage\jarLog\8f34f6bbde5f40bbb375b5d35f8fcf0c\jar-manage\jar-manage.log
日志效果,可以看到在应用启动时候,main(springboot会执行main方法)方法之前,把代理包的信息输出来了
看下MBean是否真注册成功
ok,ServerIpPortObjectMBean信息获取正常。
然后到启动不带端口参数的,这就另外一个逻辑用修改FileEncodingApplicationListener类里面注册ServerIpPortObjectMBean
启动参数:
java -javaagent:D:/springboot-agent-premain.jar -Dfile.encoding=utf-8 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote= -Dcom.sun.management.jmxremote.port=12583 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -jar D:\jarmanage\jarManage\jarStroage\8f34f6bbde5f40bbb375b5d35f8fcf0c\jar-manage\jar-manage.jar >> D:\jarmanage\jarManage\jarLog\8f34f6bbde5f40bbb375b5d35f8fcf0c\jar-manage\jar-manage.log
可以看到启动时候日志输出,执行了FileEncodingApplicationListener信息输出,并且打印出真实的应用端口是8877。
然后在远程调用看看MBean信息是否注册OK。
远程获取OK。
总结
本章易错点
1、premain的方法,没有按照规范写public static void premain,静态并且无返回值。
2、代理包的打包,没有指定Premain-Class、Agent-Class,如果是pom文件可以在pom里面配置
<build>
<finalName>springboot-agent-premain</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>agent.IpPortPremainAgent</Premain-Class>
<Agent-Class>agent.IpPortPremainAgent</Agent-Class>
<!-- <Agent-Class>agent.IpPortAgentMainAgent</Agent-Class> -->
<!-- <Can-Redefine-Classes>true</Can-Redefine-Classes> -->
<!-- <Can-Retransform-Classes>true</Can-Retransform-Classes> -->
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
指定写premain方法的类。
随便把agentmain的pom配置贴出
<build>
<finalName>springboot-agent-premain</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>agent.IpPortAgentMainAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
3、添加额外的依赖包appendToBootstrapClassLoaderSearch,这里也很容易出错,自己写的jar不能带有其它jar,只是简单的编译jar包
4、不要妄想在premain里面的Instrumentation直接获取目标应用运行时的属性。java代理只是提供修改、替换类的机会,获取不到运行时的状态。(agentmain 不能无中生有,修改的类根本没有这个方法)
5、其它。。。
拓展
除了premain之后,还可以用agentmain,虽然都是代理也能实现相同功能但是运行时机却不一样。另外个人更加偏向agentmain比较灵活,以VirtualMachine远程添加代理包并且是多次。
package com.sakyoka.test.commons.utils;
import java.io.File;
import com.sun.tools.attach.VirtualMachine;
* 描述:虚拟机工具类
* @author sakyoka
* @date 2022年6月9日 下午2:24:10
public class VirtualMachineUtils {
* 描述:执行代理包
* @author sakyoka
* @date 2022年6月9日 下午12:34:44
* @param pid 目标应用的进程号
* @param jarPath 代理包绝对路径
public static void invokeAgentmain(String pid, String jarPath){
try {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(jarPath);
} catch (Exception e) {
throw new RuntimeException(e);