日常记录:java启动参数 -javaagent的使用,应用启动前添加代理包并且注册Mbean

前言

有时候是不是很苦恼想在不修改别人的应用(或者统一处理所有的应用)情况下如何添加额外功能?那么-javaagent启动参数就能处理这个问题。

还有上一章讲的(日常记录:java 注册以及获取MBean,获取应用端口、ip_sakyoka的博客-CSDN博客),如何在不去修改每个应用情况下达到注册MBean到JMX上,本章就是后续怎么实现了。

一、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完毕。");		
	}
}

主要思路是在目标用main方法执行前时候

    1、如果-javaagent有传端口号,就默认使用改参数,直接注册ServerIpPortObject(MBean)。

    2、如果没有端口参数,那么使用修改目标应用里面一个类获取端口注册Mbean。

        没有端口参数处理过程:

         1)、使用Instrumentation的appendToBootstrapClassLoaderSearch添加一个带有ServerIpPortObject、ServerIpPortObjectMBean的jar包作为目标应用上的依赖包(注意这个jar包只包含基本编译文件,不包含依赖的jar),为什么需要添加这个jar包到目标的ClassLoader,因为后面修改的类方法中需要把Mbean注册到JMX,这就是为什么目标应用都没有MBean却又可以注册,这就是动态插入代码。

         2)、因为目标应用是用springboot原因,找到一个通用的类FileEncodingApplicationListener一个spring的文件编码监听器(按照实际选择需要修改的类),然后把监听器默认执行的方法中注册Mbean

FileEncodingApplicationListener:

 * Copyright 2012-2016 the original author or authors.
 *
 * 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.
 *
 * <p>
 * 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);
		} 
    }
}

  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值