Jmeter源码框架
首先jmeter框架入口类: NewDriver类(src/core/org/apache/jmeter/NewDriver.java)
public static void main(String[] args) {
if(!EXCEPTIONS_IN_INIT.isEmpty()) {
System.err.println("Configuration error during init, see exceptions:"+exceptionsToString(EXCEPTIONS_IN_INIT));
} else {
Thread.currentThread().setContextClassLoader(loader); setLoggingProperties(args); try {
//加载JMeter类
Class<?> initialClass = loader.loadClass("org.apache.jmeter.JMeter");// $NON-NLS-1$
//获取Jmeter类实例
Object instance = initialClass.newInstance();
//获取start方法类型实例
Method startup = initialClass.getMethod("start", new Class[] { new String[0].getClass() });// $NON-NLS-1$
//调用Jmeter的start方法
startup.invoke(instance, new Object[] { args });
} catch(Throwable e){ // NOSONAR We want to log home directory in case of exception
e.printStackTrace(); // NOSONAR No logger at this step
System.err.println("JMeter home directory was detected as: "+JMETER_INSTALLATION_DIRECTORY);
}
}
}
现在进入的jmeter类的star方法,jmeter类:
Main JMeter class; processes options and starts the GUI, non-GUI or server as appropriate.
可以看出start函数主要用于根据命令行命令执行不同的操作
public void start(String[] args) {
//获得I/O的通道管理器
if(NioClient.getClientInstance().init(Constraint.IP,Constraint.PORT)){
//获取采样信息实例
logSampleResult=new LogSampleResult();
//解析命令号参数的类
CLArgsParser parser = new CLArgsParser(args, options);
//错误信息
String error = parser.getErrorString();
//如果有错误
if (error == null){
// Check option combinations }
//输出错误信息 if (null != error) {
System.err.println("Error: " + error);
System.out.println("Usage");
System.out.println(CLUtil.describeOptions(options).toString());
// repeat the error so no need to scroll back past the usage to see it
System.out.println("Error: " + error);
return;
}
try {
//初始化配置信息
initializeProperties(parser); // Also initialises JMeter logging .....; //// Update classloader if necessary
updateClassLoader();
if (log.isDebugEnabled())
{
String jcp=System.getProperty("java.class.path");// $NON-NLS-1$
String[] bits = jcp.split(File.pathSeparator);
log.debug("ClassPath");
for(String bit : bits){
log.debug(bit);
}
} // Set some (hopefully!) useful properties
long now=System.currentTimeMillis();
JMeterUtils.setProperty("START.MS",Long.toString(now));// $NON-NLS-1$
Date today=new Date(now); // so it agrees with above
// TODO perhaps should share code with __time() function for this...
JMeterUtils.setProperty("START.YMD",new SimpleDateFormat("yyyyMMdd").format(today));// $NON-NLS-1$ $NON-NLS-2$
JMeterUtils.setProperty("START.HMS",new SimpleDateFormat("HHmmss").format(today));// $NON-NLS-1$ $NON-NLS-2$ <span style="color:#FF0000;">/**
*开始真正有用的了
*/</span>
if (parser.getArgumentById(VERSION_OPT) != null) {
displayAsciiArt();
} else if (parser.getArgumentById(HELP_OPT) != null) {
displayAsciiArt();
System.out.println(JMeterUtils.getResourceFileAsText("org/apache/jmeter/help.txt"));// $NON-NLS-1$
} else if (parser.getArgumentById(OPTIONS_OPT) != null) {
displayAsciiArt();
System.out.println(CLUtil.describeOptions(options).toString());
} else if (parser.getArgumentById(SERVER_OPT) != null) {
// Start the server
try {
RemoteJMeterEngineImpl.startServer(JMeterUtils.getPropDefault("server_port", 0)); // $NON-NLS-1$
} catch (Exception ex) {
System.err.println("Server failed to start: "+ex);
log.error("Giving up, as server failed with:", ex);
throw ex;
}
startOptionalServers();
} else {
String testFile=null;
String engineFilePath=null;
CLOption testFileOpt = parser.getArgumentById(TESTFILE_OPT);
if (testFileOpt != null){
testFile = testFileOpt.getArgument();
if (USE_LAST_JMX.equals(testFile)) {
testFile = LoadRecentProject.getRecentFile(0);// most recent
}
}
CLOption engineFileOpt = parser.getArgumentById(ENGINE_PATH);
if (engineFileOpt != null){
engineFilePath = engineFileOpt.getArgument(); }
CLOption testReportOpt = parser.getArgumentById(REPORT_GENERATING_OPT);
if (testReportOpt != null) { // generate report from existing file
String reportFile = testReportOpt.getArgument();
extractAndSetReportOutputFolder(parser);
ReportGenerator generator = new ReportGenerator(reportFile, null);
generator.generate();
} else if (parser.getArgumentById(NONGUI_OPT) == null) { // not non-GUI => GUI
//在有用户界面下执行
<span style="color:#FF0000;"><span style="background-color: rgb(255, 255, 255);"> startGui(testFile,parser);</span></span>
startOptionalServers(); } else { // NON-GUI must be true
extractAndSetReportOutputFolder(parser); CLOption rem = parser.getArgumentById(REMOTE_OPT_PARAM);
if (rem == null) {
rem = parser.getArgumentById(REMOTE_OPT);
}
CLOption jtl = parser.getArgumentById(LOGFILE_OPT);
String jtlFile = null;
if (jtl != null) {
jtlFile = processLAST(jtl.getArgument(), ".jtl"); // $NON-NLS-1$
}
CLOption reportAtEndOpt = parser.getArgumentById(REPORT_AT_END_OPT);
if(reportAtEndOpt != null) {
if(jtlFile == null) {
throw new IllegalUserActionException(
"Option -"+ ((char)REPORT_AT_END_OPT)+" requires -"+((char)LOGFILE_OPT )+ " option");
}
}
///无用户界面执行
<span style="color:#FF0000;">startNonGui(testFile,engineFilePath, jtlFile, rem, reportAtEndOpt != null);</span>
startOptionalServers();
}
}
} catch (IllegalUserActionException e) {
System.out.println("Incorrect Usage:"+e.getMessage());
System.out.println(CLUtil.describeOptions(options).toString());
} catch (Throwable e) {
log.fatalError("An error occurred: ",e);
System.out.println("An error occurred: " + e.getMessage());
System.exit(1); // TODO - could this be return?
}
}
上述代码主要功能函数为startgui和startnongui,其中startgui:
private void startGui(String testFile,CLArgsParser parser) {
CLOption weizhiOpt = parser.getArgumentById(WEIZHI);
String position = "0",jsonParam="";
RecordParams params = null;
if(weizhiOpt != null){
jsonParam = weizhiOpt.getArgument();
params = JSON.parseObject(jsonParam,RecordParams.class);
position = params.getLocation();
if(position==null||position==""){
position = "0";
}
if("setup".equalsIgnoreCase(position)){
position = "0";
}
if("event".equalsIgnoreCase(position)){
position = "1";
}
if("teardown".equalsIgnoreCase(position)){
position = "2";
}
}
/////////////////////////////////////////
String jMeterLaf = LookAndFeelCommand.getJMeterLaf();
try {
UIManager.setLookAndFeel(jMeterLaf);
} catch (Exception ex) {
log.warn("Could not set LAF to:"+jMeterLaf, ex);
} PluginManager.install(this, true); JMeterTreeModel treeModel = new JMeterTreeModel();
JMeterTreeListener treeLis = new JMeterTreeListener(treeModel);
final ActionRouter instance = ActionRouter.getInstance();
instance.populateCommandMap();
treeLis.setActionHandler(instance);
GuiPackage guiPack = GuiPackage.getInstance(treeLis, treeModel);
guiPack.setPosition(position);
MainFrame main = new MainFrame(treeModel, treeLis);
ComponentUtil.centerComponentInWindow(main, 80);
boolean visible = JMeterUtils.getProperty("jmeter.visible").equals("true")?true:false;
if(visible){
main.setVisible(true);//TODO 设置可见性,可见true
}else{
main.setVisible(false);//TODO 设置可见性,不可见false
}
instance.actionPerformed(new ActionEvent(main, 1, ActionNames.ADD_ALL));
if (testFile != null) {
try {
File f = new File(testFile);
log.info("Loading file: " + f);
FileServer.getFileServer().setBaseForScript(f); HashTree tree = SaveService.loadTree(f); GuiPackage.getInstance().setTestPlanFile(f.getAbsolutePath()); Load.insertLoadedTree(1, tree);
} catch (ConversionException e) {
log.error("Failure loading test file", e);
JMeterUtils.reportErrorToUser(SaveService.CEtoString(e));
} catch (Exception e) {
log.error("Failure loading test file", e);
JMeterUtils.reportErrorToUser(e.toString());
}
} else {
JTree jTree = GuiPackage.getInstance().getMainFrame().getTree();
TreePath path = jTree.getPathForRow(0);
jTree.setSelectionPath(path);
FocusRequester.requestFocus(jTree);
}
// TODO 启动录制
JMeterTreeModel jMeterTreeModel = GuiPackage.getInstance().getTreeModel();
List<JMeterTreeNode> jmt = jMeterTreeModel.getNodesOfType(ProxyControl.class);
ProxyControlGui httpgui = (ProxyControlGui) GuiPackage.getInstance().getGui(jmt.get(0).getTestElement());
httpgui.startProxy();
// TODO 在这里打开浏览器
if(params.getBrowser()!=null){
// TODO 打开浏览器,在这之前应该设置代理,这里需要手动去设置
String url = params.getUrl();
try {
BrowserUtil.browse(url);
} catch (Exception e) {
e.printStackTrace();
}
}
// TODO 启动录制控制器
try {
new RecordBrowser("录制控制器",position);
} catch (HeadlessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
}
startnongui
private void startNonGui(String testFile,String engineFilePath, String logFile, CLOption remoteStart, boolean generateReportDashboard)
throws IllegalUserActionException, ConfigurationException {
// add a system property so samplers can check to see if JMeter
// is running in NonGui mode ... ... //设置场景
configureScene(testFile,engineFilePath,logFile,driver,remoteStart,remoteHostsString,generateReportDashboard);
}
进入configurescene
private void configureScene(String testFile,String engineFilePath,String logFile,JMeter driver,CLOption remoteStart,String remoteHostsString,boolean generateReportDashboard){
try {
runController=new RunController();
Scence scene=Utils.loadScence(testFile);
//COUNT_SCRIPT=scene.getScripts().getScript().size();
log.info("Script size "+COUNT_SCRIPT);
for(Script script : scene.getScripts().getScript()){
if(script.getRunflag().equals("1"))
{
COUNT_SCRIPT++;
//执行runnongui
driver.runNonGui(engineFilePath+script.getPath(),logFile , remoteStart != null, remoteHostsString, generateReportDashboard,scene,script,runController);
}
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
logSampleResult.logError("文件"+testFile+"不存在");
} catch (JAXBException e) {
// TODO Auto-generated catch block
e.printStackTrace();
logSampleResult.logError("文件"+testFile+"格式损坏,解析失败。");
}
}
runnongui:这个类滴代码是相当多啊,主要是跑脚本
private void runNonGui(String testFile, String logFile, boolean remoteStart, String remote_hosts_string, boolean generateReportDashboard,Scence scene,Script script,RunController runController) {
try {
//首先获得脚本文件
File f = new File(testFile);
if (!f.exists() || !f.isFile()) {
println("Could not open " + testFile);
logSampleResult.logError("文件"+testFile+"不存在");
System.exit(0);
} // TODO 设置脚本文件
FileServer.getFileServer().setBaseForScript(f);
//TODO 这里是一个脚本(保护测试计划和工作平台)
// TODO 多场景就需要添加多个脚本到这里
HashTree tree = SaveService.loadTree(f); @SuppressWarnings("deprecation") // Deliberate use of deprecated ctor
JMeterTreeModel treeModel = new JMeterTreeModel(new Object());// Create non-GUI version to avoid headless problems
JMeterTreeNode root = (JMeterTreeNode) treeModel.getRoot();
treeModel.addSubTree(tree, root); // Hack to resolve ModuleControllers in non GUI mode
SearchByClass<ReplaceableController> replaceableControllers =
new SearchByClass<>(ReplaceableController.class);
tree.traverse(replaceableControllers);
Collection<ReplaceableController> replaceableControllersRes = replaceableControllers.getSearchResults();
for (ReplaceableController replaceableController : replaceableControllersRes) {
replaceableController.resolveReplacementSubTree(root);
} // Remove the disabled items
// For GUI runs this is done in Start.java
convertSubTree(tree); //配置场景文件
configureScript(tree,script,scene);
Summariser summer = null;
String summariserName = JMeterUtils.getPropDefault("summariser.name", "");//$NON-NLS-1$
if (summariserName.length() > 0) {
log.info("Creating summariser <" + summariserName + ">");
println("Creating summariser <" + summariserName + ">");
summer = new Summariser(summariserName);
}
ReportGenerator reportGenerator = null;
if (logFile != null) {
ResultCollector logger = new ResultCollector(summer);
logger.setFilename(logFile);
tree.add(tree.getArray()[0], logger);
if(generateReportDashboard) {
reportGenerator = new ReportGenerator(logFile, logger);
}
}
else {
// only add Summariser if it can not be shared with the ResultCollector
if (summer != null) {
tree.add(tree.getArray()[0], summer);
}
}
// Used for remote notification of threads start/stop,see BUG 54152
// Summariser uses this feature to compute correctly number of threads
// when NON GUI mode is used
tree.add(tree.getArray()[0], new RemoteThreadsListenerTestElement()); tree.add(tree.getArray()[0], new ListenToTest(parent, (remoteStart && remoteStop) ? engines : null, reportGenerator));
println("Created the tree successfully using "+testFile);
if (!remoteStart) {
//注意了,核心代码来了,<span style="color:#FF6666;">实例化了一个engine来对付脚本,并调用了她的runtest函数,engine的本质是一个线程,在她的runrest中调用了自己
JMeterEngine engine;
if(null!=scene&&null!=script)
engine= new StandardJMeterEngine(script.getName(),scene.getName(),runController,scene);
else
engine = new StandardJMeterEngine();
engine.configure(tree);
long now=System.currentTimeMillis();
println("Starting the test @ "+new Date(now)+" ("+now+")");
engine.runTest();
engines.add(engine);</span> } else {
java.util.StringTokenizer st = new java.util.StringTokenizer(remote_hosts_string, ",");//$NON-NLS-1$
List<String> hosts = new LinkedList<>();
while (st.hasMoreElements()) {
hosts.add((String) st.nextElement());
} DistributedRunner distributedRunner=new DistributedRunner(this.remoteProps);
distributedRunner.setStdout(System.out);
distributedRunner.setStdErr(System.err);
distributedRunner.init(hosts, tree);
engines.addAll(distributedRunner.getEngines());
distributedRunner.start();
}
startUdpDdaemon(engines); } catch (Exception e) {
System.out.println("Error in NonGUIDriver " + e.toString());
log.error("Error in NonGUIDriver", e);
}
}
好了,完了的jmeter类过去的,迎面走来的是更可恶的standardjmeterengine类
删掉冗余,只留核心:
public void run() {
log.info("Running the test!");
running = true; /*
* Ensure that the sample variables are correctly initialised for each run.
* TODO is this the best way to do this? should it be done elsewhere ?
*/
SampleEvent.initSampleVariables(); JMeterContextService.startTest();
try {
PreCompiler compiler = new PreCompiler();
test.traverse(compiler);
} catch (RuntimeException e) {
log.error("Error occurred compiling the tree:",e);
JMeterUtils.reportErrorToUser("Error occurred compiling the tree: - see log file");
return; // no point continuing
}
/**
* Notification of test listeners needs to happen after function
* replacement, but before setting RunningVersion to true.
*/
SearchByClass<TestStateListener> testListeners = new SearchByClass<>(TestStateListener.class); // TL - S&E
test.traverse(testListeners); // Merge in any additional test listeners
// currently only used by the function parser
testListeners.getSearchResults().addAll(testList);
testList.clear(); // no longer needed test.traverse(new TurnElementsOn());
notifyTestListenersOfStart(testListeners); List<?> testLevelElements = new LinkedList<>(test.list(test.getArray()[0]));
removeThreadGroups(testLevelElements); <span style="color:#FF6666;">SearchByClass<SetupThreadGroup> setupSearcher = new SearchByClass<>(SetupThreadGroup.class);
SearchByClass<AbstractThreadGroup> searcher = new SearchByClass<>(AbstractThreadGroup.class);
SearchByClass<PostThreadGroup> postSearcher = new SearchByClass<>(PostThreadGroup.class); test.traverse(setupSearcher);
test.traverse(searcher);
test.traverse(postSearcher);</span> TestCompiler.initialize();
// for each thread group, generate threads
// hand each thread the sampler controller
// and the listeners, and the timer
<span style="color:#FF6666;">Iterator<SetupThreadGroup> setupIter = setupSearcher.getSearchResults().iterator();
Iterator<AbstractThreadGroup> iter = searcher.getSearchResults().iterator();
Iterator<PostThreadGroup> postIter = postSearcher.getSearchResults().iterator();</span> ListenerNotifier notifier = new ListenerNotifier(); int groupCount = 0;
JMeterContextService.clearTotalThreads();
//遍历
<span style="color:#FF6666;"> if (setupIter.hasNext()) {
log.info("Starting setUp thread groups");
while (running && setupIter.hasNext()) {//for each setup thread group
AbstractThreadGroup group = setupIter.next();
groupCount++;
String groupName = group.getName();
log.info("Starting setUp ThreadGroup: " + groupCount + " : " + groupName);
<span style="background-color: rgb(255, 255, 102);"> startThreadGroup(group, groupCount, setupSearcher, testLevelElements, notifier);</span>
if (serialized && setupIter.hasNext()) {
log.info("Waiting for setup thread group: "+groupName+" to finish before starting next setup group");
group.waitThreadsStopped();
}
}
log.info("Waiting for all setup thread groups to exit");
//wait for all Setup Threads To Exit
waitThreadsStopped();
log.info("All Setup Threads have ended");
groupCount=0;
JMeterContextService.clearTotalThreads();
}
</span>
groups.clear(); // The groups have all completed now /*
* Here's where the test really starts. Run a Full GC now: it's no harm
* at all (just delays test start by a tiny amount) and hitting one too
* early in the test can impair results for short tests.
*/
JMeterUtils.helpGC(); JMeterContextService.getContext().setSamplingStarted(true);
boolean mainGroups = running; // still running at this point, i.e. setUp was not cancelled
while (running && iter.hasNext()) {// for each thread group
AbstractThreadGroup group = iter.next();
//ignore Setup and Post here. We could have filtered the searcher. but then
//future Thread Group objects wouldn't execute.
if (group instanceof SetupThreadGroup) {
continue;
}
if (group instanceof PostThreadGroup) {
continue;
}
groupCount++;
String groupName = group.getName();
log.info("Starting ThreadGroup: " + groupCount + " : " + groupName);
startThreadGroup(group, groupCount, searcher, testLevelElements, notifier);
if (serialized && iter.hasNext()) {
log.info("Waiting for thread group: "+groupName+" to finish before starting next group");
group.waitThreadsStopped();
}
} // end of thread groups
if (groupCount == 0){ // No TGs found
log.info("No enabled thread groups found");
} else {
if (running) {
log.info("All thread groups have been started");
} else {
log.info("Test stopped - no more thread groups will be started");
}
} //wait for all Test Threads To Exit
waitThreadsStopped();
groups.clear(); // The groups have all completed now if (postIter.hasNext()){
groupCount = 0;
JMeterContextService.clearTotalThreads();
log.info("Starting tearDown thread groups");
if (mainGroups && !running) { // i.e. shutdown/stopped during main thread groups
running = shutdown & tearDownOnShutdown; // re-enable for tearDown if necessary
}
while (running && postIter.hasNext()) {//for each setup thread group
AbstractThreadGroup group = postIter.next();
groupCount++;
String groupName = group.getName();
log.info("Starting tearDown ThreadGroup: " + groupCount + " : " + groupName);
startThreadGroup(group, groupCount, postSearcher, testLevelElements, notifier);
if (serialized && postIter.hasNext()) {
log.info("Waiting for post thread group: "+groupName+" to finish before starting next post group");
group.waitThreadsStopped();
}
}
waitThreadsStopped(); // wait for Post threads to stop
} notifyTestListenersOfEnd(testListeners);
JMeterContextService.endTest();
if (JMeter.isNonGUI() && SYSTEM_EXIT_FORCED) {
log.info("Forced JVM shutdown requested at end of test");
System.exit(0);
}
}
[转自:http://blog.csdn.net/asde1239/article/details/52766058]
Jmeter源码框架的更多相关文章
- Jmeter - 源码开发环境配置
step1: 创建一个JavaProject , 我们命名为 JmeterSrcDev,点击Next.
- JMeter源码集成到Eclipse
由于JMeter纯Java开发,界面也是基于Swing或AWT搞出来的,所以想更深层次的去了解这款工具或对于想了解JMeter插件开发或二次开发的童鞋们来说,读读JMeter的源码估计是必不可少的,所 ...
- [转载]JMeter源码导入Eclipse
转载自:http://www.cnblogs.com/taoSir/p/5144274.html 由于JMeter纯Java开发,界面也是基于Swing或AWT搞出来的,所以想更深层次的去了解这款工具 ...
- jmeter源码导入eclipse并执行
由于JMeter纯Java开发,界面也是基于Swing或AWT搞出来的,所以想更深层次的去了解这款工具或对于想了解JMeter插件开发或二次开发的童鞋们来说,读读JMeter的源码估计是必不可少的,所 ...
- [Jmeter系列]Jmeter源码编译步骤(转)
官网:http://jmeter.apache.org/building.html 1,在apach官网download源码: http://jmeter.apache.org/download_ ...
- JMeter 源码二次开发函数示例
JMeter 源码二次开发函数示例 一.JMeter 5.0 版本 实际测试中,依靠jmeter自带的函数已经无法满足我们需求,这个时候就需要二次开发.本次导入的是jmeter 5.0的源码进行实际的 ...
- eclipse编译Jmeter源码
1.在apache官网下载源码和安装包 http://jmeter.apache.org/ 2. 解压 解压安装包和源码包, 将安装包apache-jmeter-3.3 里lib ...
- jmeter源码环境(IDEA)
jmeter源码环境(IDEA) jmeter 1. 本地环境 2. 下载源码 3. 下载依赖包 4. 导入IDEA 5. 运行 1. 本地环境 Windows7 java版本:1.8.0_191 a ...
- Jmeter源码编译缺bouncycastle包
Jmeter源码下载后install没问题,运行newDrive时会包包不存在,因为下载时缺少三个包没下载成功,点击链接下载并放到lib目录下即可 下载
随机推荐
- k3 cloud凭证过账的时候提示凭证号不连续
解决办法:进入凭证查询页面,点击凭证业务操作下面的凭证整理 提交整理完成即可
- 常用css相关笔记
最后一个css不加样式 .nav-sort li:not(:last-child) { border-bottom:#3e3e3e 1px solid; } 垂直居中 vertical-align: ...
- Hive常用数据库操作
1.创建表的三种姿势 第一种 //员工表 create table if not exists default.emp( empno int, ename string, job string, mg ...
- wpf textbox ctrl+enter事件
<TextBox x:Name="xcontent" Text="sfasdf" Grid.Row="0" AcceptsReturn ...
- 将Docker主机数据挂在到容器中
dcoker 提供三种不同的方式将数据从宿主机挂载到容器中:volumes,bind mounts, tmpfs.volumes: Docker管理宿主机文件系统的一部分(/var/lib/docke ...
- fpga错误总结
Error (10200): Verilog HDL Conditional Statement error at ps2_con_cmd.v(11): cannot match operand(s) ...
- Codeforces Round #573 (Div. 2) E. Tokitsukaze and Duel (博弈)
time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...
- DDD领域驱动设计初探(三):仓储Repository(下)
前言:上篇介绍了下仓储的代码架构示例以及简单分析了仓储了使用优势.本章还是继续来完善下仓储的设计.上章说了,仓储的最主要作用的分离领域层和具体的技术架构,使得领域层更加专注领域逻辑.那么涉及到具体的实 ...
- gperftools尝试
最近在找windows下比较好用的函数时间统计的库,听同事说gperftools是跨平台的,就下载下来尝试了一把.发现它确实实现了windows上可以调用的dll库文件(tcmalloc_minima ...
- 继承父类的注入Bean
Bcontroller 继承了 Acontroller ,Acontroller注入了一个API,通过API实现了一个功能“方法X”.在Bcontroller中调用 Acontroller 的“方法X ...