起因就是组内旧项目也要接入现有的微服务部署,该项目大致上就是后台MVC和前端JSP写在了一起,还用了一堆shiro、mybatis-plus这些组件,整改过程想必问题很多,所以写一篇博客,事无巨细,都简单地记录下来。

原项目目录结构如图:        

springjava项目怎么改造为springmaven spring改造springboot_spring boot

被我一顿换后,改了pom.xml文件加了启动类,有点boot项目的样子了,项目结构如图:

springjava项目怎么改造为springmaven spring改造springboot_jar包_02

其实就是原封不动的复制过来,只在资源文件夹里加了application.xml,并且java文件夹项目路径中写了启动类,再调了调文件夹类型而已。先启动再说吧,接下来就是一个一个的报错解决。

一.spring boot版本不合适

springjava项目怎么改造为springmaven spring改造springboot_spring boot_03

 点异常栈信息最后知道是因为缺少DataSize这个类,这个类是在spring-core这个spring核心jar里面的,项目中该jar的版本是4.3.8.RELEASE,这个版本中确实没有这个类。一开始我使用的spring-boot-starter是2.1.9.RELEASE版本,这个版本默认spring核心jar包都是5.1.10.RELEASE,这个版本是适配的里面有DataSize,我也尝试过将spring核心jar包版本都换成5.1.10.RELEASE,但项目多处直接报错,看来原则就是尽量不改项目原有的东西,那就只能改spring-boot-starter的版本喽,最后在maven repository里找到了spring-boot-starter的1.4.6.RELEASE版本,这个版本的spring核心jar包都是4.3.8.RELEASE,这样果然就ok了,好吧,将就用吧。

二.编译语言版本不够导致编译报错

springjava项目怎么改造为springmaven spring改造springboot_shiro_04

这个简单,将maven编译插件指定语言版本就行了,如下:

springjava项目怎么改造为springmaven spring改造springboot_jar包_05

这种问题在IDEA里面好像也可以设置 ,不过这种方式还是最稳的。

三.pom.xml里面如果有重复依赖遵循路径最短,申明顺序其次原则。

报错就是代码里有一个地方Cookie的setHttpOnly这个方法不存在,想必又是jar版本的问题。看了一下原项目Cookie这个类所在的jar版本是3.1.0,但是我也没改这个jar包版本啊,看了一下现在项目的maven的依赖树,发现Cookie这个类所在的jar包版本居然变成了2.4,后来发现其实这两次看的不是一个jar包,原来是因为项目中同时引用了servlet-api和javax.servlet-api这两个jar包,但是这两个jar包的类路径都是相同:

springjava项目怎么改造为springmaven spring改造springboot_tomcat_06

springjava项目怎么改造为springmaven spring改造springboot_spring boot_07

 pom.xml里面如果有重复依赖遵循路径最短,申明顺序其次原则。原项目中pom.xml中的引用是3.1.0的javax.servlet-api在先而2.4的servlet-api在后,这里两个jar包都是直接引用所以路径相同,所以原项目用的是声明顺序在先的3.1.0的javax.servlet-api,而我本着pom文件太难看不经意的把这两个换了一下顺序,就导致报这个错,好吧,先不追究为什么要引用两个相同路径的jar包,看来雷真不少,原项目的东西动哪里都要慎重。

四.这个版本的spring boot居然还需要自己引入tomcat核心jar包。

springjava项目怎么改造为springmaven spring改造springboot_jar包_08

根据异常栈信息可以定位到getEmbeddedServletContainerFactory()抛的异常,看一下这个方法:

springjava项目怎么改造为springmaven spring改造springboot_jar包_09

 beanFactory可以理解为bean容器,话说beanFactory和factoryBean得区分一下,这里不扩展。看来这里是因为从bean容器中没有拿到EmbeddedServletContainerFactory这个bean,length为0所以向上抛的异常,这是个接口,通过继承关系拓扑图可以看到这个接口有这么几个实现类:

springjava项目怎么改造为springmaven spring改造springboot_spring_10

现在用的容器就是tomcat,其他两个实现类就不看了,看一下这个TomcatEmbeddedServletContainerFactory类:

springjava项目怎么改造为springmaven spring改造springboot_tomcat_11

我擦,这是这个类里面的引用,居然一片飘红,这能创建实例有鬼啊,仔细一看这不是tomcat的核心jar包嘛,spring boot是内嵌了tomcat,但是难不成这核心jar包还需要自己引用?自己引就自己引吧,先去看一下这个版本的spring boot内嵌的tomcat引的jar包时哪个版本,一顿点之后找到这个:

springjava项目怎么改造为springmaven spring改造springboot_tomcat_12

也不知道这个版本有什么特殊的,去maven repository里找到了tomcat的几个核心jar包,反正也不知道该用哪个版本好,不管灵不灵,那就都用这个版本吧,如下:

springjava项目怎么改造为springmaven spring改造springboot_jar包_13

 下面两个是为jsp引的,可能和这次报错无关。项目中前端用jsp写的,必然有大量的el表达式,本着另可错杀一千不能放过一个的原则就都拉过来了。这么一来问题还真解决了,有点懵的成分,若有上帝视角的大佬还请指出根因。

五.spring boot适配用xml注册bean

报错就是XXXMapper找不到这个bean,贴一下报错截取部分:

springjava项目怎么改造为springmaven spring改造springboot_spring_14

mapper找不到bean无非两个原因,要么是没有打@Repository,要么就是打了注解没有扫描到,检查了一下每个Mapper文件上注解都打了,想了一下确实没有在启动类上打@MapperScan注解,如果没有这个注解spring会启动类当前文件夹下找Mapper文件,那现在就是要打上@MapperScan注解并指定好Mapper文件的路径,如图:

springjava项目怎么改造为springmaven spring改造springboot_spring boot_15

这样确实解决了问题,可是原项目肯定也要扫描mapper文件也要注册bean啊,原项目是用XML的方式去注册bean的,找了找在一个spring-mybatis.xml文件中看到了这个:

springjava项目怎么改造为springmaven spring改造springboot_shiro_16

这是一个典型的以前用XML方式注册bean的方式,SpringBoot也同样提供了手动加载XML配置中的bean的方法,只需要自定义一个类,打上@Configuration注解它就是配置类了,然后用@ImportResource引用xml配置,如图:

springjava项目怎么改造为springmaven spring改造springboot_tomcat_17

 类里什么都不用写,这样xml文件中注册的bean就生效了,本着尽量沿用原项目的原则,采用了第二种方式,然后我一股脑的把这种注册bean的xml文件都添加进了locations,以绝后患。

六.我放弃了打成jar包,改打war包

接下来的一天太痛苦了,本意是想将项目打成jar包,由于项目中有jsp,但是似乎高版本的spring boot内置的tomcat已经放弃对jsp的支持了,这次尝试的1.4.6.RELEASE版本就已经有一堆问题了,也尝试了各种自己引依赖,启动的时候各种报错看不懂,加上原项目中的各种依赖版本冲突,哇心态爆炸,退而求其次那就打war包吧,本质上就是不用jar包中内置的tomcat,既然这样,那前面第四点引入的依赖就都不需要了,可以删掉,白写。接下来是spring boot项目打war包需要的改动:

1.首先修改pom.xml中的<packaging>标签为war

springjava项目怎么改造为springmaven spring改造springboot_shiro_18

2.同样是在pom.xml中添加maven-war-plugin插件,我这里就为了定义一下名字,还可以做其他的

springjava项目怎么改造为springmaven spring改造springboot_spring_19

3.排除spring boot中的内置tomcat

springjava项目怎么改造为springmaven spring改造springboot_shiro_20

4.修改一下启动类,继承SpringBootServletInitializer并重写configure()方法:

springjava项目怎么改造为springmaven spring改造springboot_tomcat_21

5.然后就可以打包部署在IDEA中的tomcat里面了 

七.让tomcat启动多打点日志

在IDEA中部署好war包后启动,报了这么一行错:

springjava项目怎么改造为springmaven spring改造springboot_jar包_22

这里报错了,可是多打点日志不好么,找了半天也不知道去哪里看详细日志,但是有办法让tomcat启动时多打点日志,在resources文件夹下新增一个logging.properties,里面这么写:

springjava项目怎么改造为springmaven spring改造springboot_spring_23

就是改一下Catalina的日志等级,配置文件位置这些都是缺省的临时用一下,重新启动后果然告诉我是哪里有问题了:

springjava项目怎么改造为springmaven spring改造springboot_tomcat_24

意思就是多了一个ContextLoaderListener,还让我检查一下web.xml里面是不是多定义了,够详细的都教我怎么解决了,早点这样打日志不好吗,去看一下web.xml果然看到定义了一个ContextLoaderListener的bean,把它注释掉呢,如下:

springjava项目怎么改造为springmaven spring改造springboot_tomcat_25

果然启动没啥问题了。一顿点之后找到了原因。介绍原因之前先解释一下webApplicationContext和root webApplicationContext。

这要从Spring的容器概念开始说起,容器概念是Spring最重要的一个概念,而且Spring有父子容器的概念,子容器中的Bean可以访问父容器中的Bean,反之则不行。其实Sping可以理解为一个父容器,Spring中引入的框架如Spring MVC可以理解成一个子容器。Spring MVC这个子容器中的Bean有前端控制器、视图解析器以及处理器适配器等,而父容器中的Bean就是那一个个的Controller、Service等,所以子容器中的处理器适配器可以去找到父容器中的对应的Controller。webApplicationContext是只有web项目里才有的,也就是引入了Spring MVC框架的org.springframework.web包 。而我们的项目一般都是Spring Web项目,可以截图看一下启动类中启动成功返回的ApplicationContext是什么:

springjava项目怎么改造为springmaven spring改造springboot_spring boot_26

可以看到这里返回的是 AnnotationConfigServletWebServerApplicationContext,也就是基于注解获取配置信息的webApplicationContext。我们通常意义上说webApplicationContext是指子容器,而root webApplicationContext则是指父容器,这里的AnnotationConfigServletWebServerApplicationContext其实就是一种root webApplicationContext父容器。

 这个ContextLoaderListener实现ServletContextListener,ServletContextListener接口里的函数会结合Web容器的生命周期被调用。因为ServletContextListener是ServletContext的监听者,如果ServletContext发生变化,会触发相应的事件。比如说在服务器启动时,ServletContext被创建的时候,服务器关闭时,ServletContext将被销毁的时候等,相当于web的生命周期创建与效果的过程。那么ContextLoaderListener的作用是什么?ContextLoaderListener的作用就是启动Web容器时,读取在contextConfigLocation中定义的xml文件,通常就是web.xml,自动装配ApplicationContext的配置信息,并产生root webApplicationContext父容器对象,然后将这个对象放置在ServletContext的属性里,这样我们只要得到Servlet就可以得到root webApplicationContex父容器对象,并利用这个对象访问spring容器管理的bean。但是不好意思,root webApplicationContex父容器对象肯定只能有一个,也就是上图中Spring Boot的启动类中的run方法,可以看到在这里面已经创建了一个ApplicationContext了,下图是SpringApplication中重载的一个run()方法,启动类中的run()方法里初始化完SpringApplication后调用了它:

springjava项目怎么改造为springmaven spring改造springboot_spring_27

可以看出run()方法的返回值就是ApplicationContext,而且是root webApplicationContex,所以这里重复了,也就不需要这个ContextLoaderListener了。

八.用XML方式配置Shiro最大的坑

因为这个问题,这个项目整合SpringBoot居然是跨季度的一件事,一直没有什么头绪,还一直都有其他事情插队,也就闲下来思考一下。先描述一下问题,项目启动之后发现所有的js、css甚至ico都无法访问,界面就成了这样:

springjava项目怎么改造为springmaven spring改造springboot_spring boot_28

看一下浏览器的控制台发现所有的静态资源请求都被重定向到了登录接口:

springjava项目怎么改造为springmaven spring改造springboot_jar包_29

 可是在XML中明明已经配置了静态资源路径shiro是不需要验证的:

springjava项目怎么改造为springmaven spring改造springboot_spring_30

这就很奇怪了,之后的一段时间就是各种看shiro。

其实问题就出在上图中最后的bean即session超时处理过滤器的配置上,可以先看一下这个过滤器:

springjava项目怎么改造为springmaven spring改造springboot_jar包_31

看这个onAccessDenied()方法:

springjava项目怎么改造为springmaven spring改造springboot_spring_32

 接下来的就不截图了,就是这个过滤器把所有的静态资源请求都重定向到了登录接口。注册Bean的时候可以看出这个ShiroAjaxSessionFilter只是ShiroFilterFactoryBean里面的一个属性值,而那些配置的需要验证和不需要验证的url也是ShiroFilterFactoryBean的属性值,可以理解成ShiroAjaxSessionFilter是ShiroFilterFactoryBean的一个下级,那ShiroAjaxSessionFilter当然得乖乖听话,配置好的静态资源路径让你不要拦就不要拦,你这个过滤器生不生效是ShiroFilterFactoryBean说了算,这么解释比较形象了。

再结合源码解释一下,源码就不贴了,简单描述一下,上图XML中一个url对应一个anon或者user,这样的配置会让Shiro管理两个过滤器,一个是beanName为user的ShiroAjaxSessionFilter,另一个是beanName为anon的AnonymousFilter,如图:

springjava项目怎么改造为springmaven spring改造springboot_spring boot_33

springjava项目怎么改造为springmaven spring改造springboot_shiro_34

这两个过滤器AnonymousFilter是shiro自己的我们不管,这个ShiroAjaxSessionFilter是我们自己注册的bean,注意他们的属性this.appliedPaths,顾名思义就是该过滤器应用于的url,这里没有展开这个属性,不过到这里应该可以理解了,ShiroFilterFactoryBean作为他们的上级,是管着他们拦他们该拦的url的,即ShiroAjaxSessionFilter不会去拦截配置为anon的url。

但是在Spring Boot中,这么配置就已经将这个过滤器交给Bean容器管理了,它的上级不在是ShiroFilterFactoryBean,不会在管配置在ShiroFilterFactoryBean中的那些url。而ShiroAjaxSessionFilter里面配置的url刚好是/**,就全部拦截了。一直没有怀疑这个是因为我记得自定义Filter让它生效除了交给Bean容器还需要注册什么的,为此我尝试了一下自定义一个过滤器直接交给Bean容器管理,在doFilter()方法中打了断点看是不是生效了:

springjava项目怎么改造为springmaven spring改造springboot_jar包_35

springjava项目怎么改造为springmaven spring改造springboot_spring boot_36

果然断点进来了,我就只注册了bean就直接生效了,原因我没有去深究,估计是和Spring Boot那一套约定大于配置有关,那ShiroAjaxSessionFilter就是同理了。

所以ShiroAjaxSessionFilter这个过滤器不能这么配置,也就是说不能交给Bean容器管理,如果沿用XML配置方式我没有想到什么好方法,但是最简单的方法就是用@Configuration配置类去配置,如下:

springjava项目怎么改造为springmaven spring改造springboot_shiro_37

这里我自己new一个ShiroAjaxSessionFilter不就好了。

重新启动后终于看上去没啥问题了:

springjava项目怎么改造为springmaven spring改造springboot_spring_38

可能还有其他问题后续有待发现,这里就算大功告成。

后续问题真的来了,继续记录:

九.Nginx默认Request Entity最大只有1M

服务部署的时候用Nginx做了代理,有一个上传文件的接口报了413错误:

springjava项目怎么改造为springmaven spring改造springboot_shiro_39

这是因为Nginx对请求报文大小做了限制,默认只有1M,如果我们要上传文件什么的肯定不够,所以需要修改一下,可以在http{}中加client_max_body_size 300m这段配置,如图:

springjava项目怎么改造为springmaven spring改造springboot_tomcat_40

这段配置不仅可以加在http块中,还可以加在server{]块中,就是作用域区别,在http块中相当于作用于全局了。重启Nginx之后就可以了:

springjava项目怎么改造为springmaven spring改造springboot_jar包_41

九.SpringBoot中的两种MultipartResolver

这个问题值得专门一篇博客。


十.font-awesome图标无法正常显示

问题如图:

springjava项目怎么改造为springmaven spring改造springboot_spring boot_42

系统中的图标用的是font-awesome,界面上却无法正常展示,都只有红色框框,是因为maven打包的时候对项目进行统一编码,但是部分特殊文件是不需要重新编码的,就例如font-awesome的这些文件,重新编码后会导致文件不可用导致对这些今天资源的访问404:

springjava项目怎么改造为springmaven spring改造springboot_spring_43

这部分不需要重新编码的静态资源只需要在pom.xml中配置对这些后缀文件不重新编码即可:

springjava项目怎么改造为springmaven spring改造springboot_spring_44

这样就可以了:

springjava项目怎么改造为springmaven spring改造springboot_jar包_45