背景

Tomcat下使用JSP作为监控手段可以非常简单的处理. 而在SpringBoot下支持的不是蛮好了,也是需要我们手动进行支持,还是使用JSP去监控SpringBoot的运行.

目标

写一个兼容SpringBoot可以访问JSP的starter. 这些JSP是作为监控手段使用,基本上不会再发生变化,非常稳定. 而SpringBoot 1.x 和 SpringBoot 2.x 的版本变化很大,最为明显的就是SpringBoot 1.x中的一些类在SpringBoot 2.x中已经变更类package. 为了减少频繁变更jar包,一个兼容 SpringBoot1和SpringBoot2的jar包还挺好的。

行动

工欲善其事,必先利其器,
Q: 我们要做什么
A: SpringBoot支持访问JSP,且对业务方无感觉

Q: Tomcat是如何找到JSP的,如何处理JSP的
A: 不知道如何找到的,将jsp编译成class文件,使用jspClassloader单独加载使用. 处理jsp部分交给tomcat的DefaultServlet进行处理

Tomcat是怎么找到JSP的

已经知道了JSP是被DefaultServlet处理的222.jpeg

兼容SpringBoot1和SpringBoot2的Starter - 图2 综上所述聚焦于WebResourceRoot是如何查找到JSP文件的。沿着这里继续深入可以看到如下结果,过程不再赘述(不同的Tomcat版本可能会有小小的改变)

兼容SpringBoot1和SpringBoot2的Starter - 图3 查找源代码如下 getResourceInternal()

  1. private final List<List<WebResourceSet>> allResources = new ArrayList<>();
  2. protected final WebResource getResourceInternal(String path,boolean useClassLoaderResources) {
  3. WebResource result = null;
  4. WebResource virtual = null;
  5. WebResource mainEmpty = null;
  6. //遍历
  7. for (List<WebResourceSet> list : allResources) {
  8. for (WebResourceSet webResourceSet : list) {
  9. if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() ||
  10. useClassLoaderResources && !webResourceSet.getStaticOnly()) {
  11. result = webResourceSet.getResource(path);
  12. if (result.exists()) {
  13. return result;
  14. }
  15. if (virtual == null) {
  16. if (result.isVirtual()) {
  17. virtual = result;
  18. } else if (main.equals(webResourceSet)) {
  19. mainEmpty = result;
  20. }
  21. }
  22. }
  23. }
  24. }
  25. // Use the first virtual result if no real result was found
  26. if (virtual != null) {
  27. return virtual;
  28. }
  29. // Default is empty resource in main resources
  30. return mainEmpty;
  31. }

如此看来,只要将JSP写入到allResources中去即可. 11.jpeg

如何注入JSP到指定地点

经过多方打探,终于找到地址,原来是 StandardRoot#createWebResourceSet 中有一个资源插入的过程.

  1. @Override
  2. public void createWebResourceSet(ResourceSetType type, String webAppMount,
  3. URL url, String internalPath) {
  4. BaseLocation baseLocation = new BaseLocation(url);
  5. //引用的这里
  6. createWebResourceSet(type, webAppMount, baseLocation.getBasePath(),
  7. baseLocation.getArchivePath(), internalPath);
  8. }
  9. //实际执行
  10. @Override
  11. public void createWebResourceSet(ResourceSetType type, String webAppMount,
  12. String base, String archivePath, String internalPath) {
  13. List<WebResourceSet> resourceList;
  14. WebResourceSet resourceSet;
  15. switch (type) {
  16. case PRE:
  17. resourceList = preResources;
  18. break;
  19. case CLASSES_JAR:
  20. resourceList = classResources;
  21. break;
  22. case RESOURCE_JAR:
  23. resourceList = jarResources;
  24. break;
  25. case POST:
  26. resourceList = postResources;
  27. break;
  28. default:
  29. throw new IllegalArgumentException(
  30. sm.getString("standardRoot.createUnknownType", type));
  31. }
  32. // This implementation assumes that the base for all resources will be a
  33. // file.
  34. File file = new File(base);
  35. if (file.isFile()) {
  36. if (archivePath != null) {
  37. // Must be a JAR nested inside a WAR if archivePath is non-null
  38. resourceSet = new JarWarResourceSet(this, webAppMount, base,
  39. archivePath, internalPath);
  40. } else if (file.getName().toLowerCase(Locale.ENGLISH).endsWith(".jar")) {
  41. resourceSet = new JarResourceSet(this, webAppMount, base,
  42. internalPath);
  43. } else {
  44. resourceSet = new FileResourceSet(this, webAppMount, base,
  45. internalPath);
  46. }
  47. } else if (file.isDirectory()) {
  48. resourceSet =
  49. new DirResourceSet(this, webAppMount, base, internalPath);
  50. } else {
  51. throw new IllegalArgumentException(
  52. sm.getString("standardRoot.createInvalidFile", file));
  53. }
  54. if (type.equals(ResourceSetType.CLASSES_JAR)) {
  55. resourceSet.setClassLoaderOnly(true);
  56. } else if (type.equals(ResourceSetType.RESOURCE_JAR)) {
  57. resourceSet.setStaticOnly(true);
  58. }
  59. resourceList.add(resourceSet);
  60. }

44.jpeg

这样依赖,我只要持有一个 WebResourceRoot 的引用即可实现找到JSP的这个过程了。 而这个引用在Tomcat中是线程的。可以用过Context可以获取到这个依赖.

兼容SpringBoot1和SpringBoot2的Tomcat

SpringBoot1 和 SpringBoot2到package换了,具体是Boot1是

  • org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer

这段代码写在 module1

  1. /**
  2. * @author chenshun00@gmail.com
  3. * @since 2018/12/27
  4. */
  5. public abstract class Boot1 {
  6. public final static Object object;
  7. static {
  8. object = new EmbeddedServletContainerCustomizer() {
  9. @Override
  10. public void customize(ConfigurableEmbeddedServletContainer container) {
  11. if (container instanceof TomcatEmbeddedServletContainerFactory) {
  12. ((TomcatEmbeddedServletContainerFactory) container).addContextCustomizers(new TomcatContextCustomizer() {
  13. @Override
  14. public void customize(Context context) {
  15. //没错 ResourceConfigurer是我加的
  16. context.addLifecycleListener(new ResourceConfigurer(context));
  17. }
  18. });
  19. }
  20. }
  21. };
  22. }
  23. }

Boot2是

  • org.springframework.boot.web.server.WebServerFactoryCustomizer

这段写在module2下

  1. public abstract class AgentObject {
  2. public static Object object;
  3. static {
  4. //没错 ResourceConfigurer是我加的
  5. object = new WebServerFactoryCustomizer<WebServerFactory>() {
  6. @Override
  7. public void customize(WebServerFactory factory) {
  8. if (factory instanceof TomcatServletWebServerFactory) {
  9. TomcatServletWebServerFactory tomcatServletWebServerFactory = (TomcatServletWebServerFactory) factory;
  10. tomcatServletWebServerFactory.addContextCustomizers(new TomcatContextCustomizer() {
  11. @Override
  12. public void customize(Context context) {
  13. context.addLifecycleListener(new ResourceConfigurer(context));
  14. }
  15. });
  16. } else {
  17. if (factory instanceof TomcatReactiveWebServerFactory) {
  18. TomcatReactiveWebServerFactory tomcatServletWebServerFactory = (TomcatReactiveWebServerFactory) factory;
  19. tomcatServletWebServerFactory.addContextCustomizers(new TomcatContextCustomizer() {
  20. @Override
  21. public void customize(Context context) {
  22. context.addLifecycleListener(new ResourceConfigurer(context));
  23. }
  24. });
  25. } else {
  26. throw new RuntimeException("未知容器,请和xxx联系修改starter实现");
  27. }
  28. }
  29. }
  30. };
  31. }
  32. }

为啥子要分开2个呢,因为他们分别依赖段是SpringBoot 1.5.15.Release 和 SpringBoot 2.0.3.Release , 如果不区分是过不了编译滴.

ResourceConfigurer 的作用就是用来传递Context ,然后将JSP代码所在目录传递进去.

  1. public class ResourceConfigurer implements LifecycleListener {
  2. private final Context context;
  3. private volatile boolean init = false;
  4. public ResourceConfigurer(Context context) {
  5. this.context = context;
  6. }
  7. @Override
  8. public void lifecycleEvent(LifecycleEvent event) {
  9. //支持 META-INF/resources 下的jsp 需要拿到主目录的那个jar包
  10. application(event);
  11. starter(event);
  12. }
  13. private void starter(LifecycleEvent event) {
  14. if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
  15. if (!init) {
  16. init = true;
  17. try {
  18. String property = System.getProperty("springboot.home", System.getProperty("user.home"));
  19. System.out.println("property ===>" + property);
  20. property = property.endsWith("/") ? property : property + "/";
  21. property = property + "code";
  22. check(property);
  23. URL url = ResourceUtils.getURL(property);
  24. context.getResources().createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", url, "/");
  25. } catch (FileNotFoundException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. }
  31. private void application(LifecycleEvent event) {
  32. try {
  33. if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
  34. URL location = ClassUtils.getDefaultClassLoader().getResource("");
  35. if (location != null) {
  36. if (ResourceUtils.isFileURL(location)) {
  37. // when run as exploded directory
  38. String rootFile = location.getFile();
  39. if (rootFile.endsWith("/BOOT-INF/classes/")) {
  40. rootFile = rootFile.substring(0, rootFile.length() - "/BOOT-INF/classes/".length() + 1);
  41. }
  42. if (!new File(rootFile, "META-INF" + File.separator + "resources").isDirectory()) {
  43. return;
  44. }
  45. try {
  46. location = new File(rootFile).toURI().toURL();
  47. } catch (MalformedURLException e) {
  48. throw new IllegalStateException("Can not add tomcat resources", e);
  49. }
  50. }
  51. String locationStr = location.toString();
  52. if (locationStr.endsWith("/BOOT-INF/classes!/")) {
  53. // when run as fat jar
  54. locationStr = locationStr.substring(0, locationStr.length() - "/BOOT-INF/classes!/".length() + 1);
  55. try {
  56. location = new URL(locationStr);
  57. } catch (MalformedURLException e) {
  58. throw new IllegalStateException("Can not add tomcat resources", e);
  59. }
  60. }
  61. this.context.getResources().createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", location, "/META-INF/resources");
  62. }
  63. }
  64. } catch (Throwable ex) {
  65. System.out.println("访问不了也没关系:" + ex.getMessage());
  66. }
  67. }
  68. private void check(String property) {
  69. File file = new File(property);
  70. if (!file.exists()) {
  71. System.err.println("错误:请检查[" + property + "]目录存不存在");
  72. System.exit(1);
  73. }
  74. }
  75. }

到这里兼容2个Tomcat是搞定了 55.jpeg
但是我们希望要是只要一个jar包就好了,而且这个jar包是通过SpringBoot到 loader.path 进行处理的,业务无感知,当然这个无法阻挡我们,将这些jar包打在一起不就可以了吗

  1. <plugin>
  2. <artifactId>maven-assembly-plugin</artifactId>
  3. <configuration>
  4. <appendAssemblyId>false</appendAssemblyId>
  5. <descriptorRefs>
  6. <descriptorRef>jar-with-dependencies</descriptorRef>
  7. </descriptorRefs>
  8. </configuration>
  9. <executions>
  10. <execution>
  11. <id>make-assembly</id>
  12. <phase>package</phase>
  13. <goals>
  14. <goal>assembly</goal>
  15. </goals>
  16. </execution>
  17. </executions>
  18. </plugin>

经历到了这里,还差最后一步,如何将Tomcat到配置叫给SpringBoot进行管理呢,如果不能进行管理上边做的事跟没做一样的.

一开始想到到是直接用@Bean进行注入,但是并没有用 因为等到你注入这个的时候,Tomcat流程都跑完了,黄花菜都凉了.

  1. @Bean
  2. public Object tomcatContext(){
  3. if (boot1){
  4. return Boot1.object;
  5. } else {
  6. return AgentObject.object;
  7. }
  8. }

不断的Google发现只能在BeanFactoryPostProcessor中进行注入了,最终实现如下

  1. public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
  2. private volatile boolean tomcat;
  3. public MyBeanFactoryPostProcessor(boolean tomcat) {
  4. this.tomcat = tomcat;
  5. }
  6. @Override
  7. public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
  8. if (tomcat) {
  9. System.out.println("tomcat+SpringBoot项目不初始化");
  10. return;
  11. }
  12. if (!init) {
  13. init = true;
  14. System.out.println("==========init MyBeanFactoryPostProcessor=========");
  15. DefaultListableBeanFactory re = (DefaultListableBeanFactory) configurableListableBeanFactory;
  16. try {
  17. re.registerSingleton("ffEmbeddedServletContainerCustomizer", ff);
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }
  23. private volatile boolean init = false;
  24. private static Object ff;
  25. static {
  26. try {
  27. if (Constant.springBoot1_5) {
  28. ff = Boot1.object;
  29. }
  30. } catch (Throwable ignored) {
  31. }
  32. try {
  33. if (Constant.springBoot2_0) {
  34. ff = AgentObject.object;
  35. }
  36. } catch (Throwable ignored) {
  37. }
  38. }
  39. }

各个module如下图

image.png

分这么多主要是解决1.x 和 2.x 不兼容的问题,最后打包通过 maven-assembly-plugin 插件打包到一次就可以了

总结

世界是科学的