从工作几乎一直在使用开箱即用的Spring Boot,很少使用Spring MVC。在上篇文章介绍Spring MVC示例中搭建那个HelloWorld demo过程中,还费了一些周折。想着可能很多人的Java Web之旅都是从Spring Boot开启的,本篇文章我们就来介绍一下如何使用Idea构建Spring MVC应用,以及通过Idea部署Spring MVC应用。
1. Idea构建并部署Spring MVC应用
1.1 创建Web应用
这里我们来看一个最简单的问题,如何通过Idea构建并部署Spring MVC应用,代码我们还用上篇文章中介绍的HelloWorld,这里重点关注如何构建并部署的过程。
这里我没有用Spring脚手架搭建,而是通过JavaEE创建Web Application方式 构建的。这样,一个空的Web Application就创建好了,如下:
由于我这里想使用Maven来管理jar,所以接下来为我们的Web应用添加Maven支持。右键应用程序名,选择“Add Framework Support”,勾选“Maven”,并点击确定:
这样我们的应用就变成一个Maven应用了。
1.2 使用Spring MVC开发应用
首先我们在pom.xml中添加Spring依赖,并通过Maven讲相关依赖包加载到应用中来:
这里之所以在“pom.xml”中添加jstl依赖,是因为我们加下来使用到了视图解析器要用到这个包,不然接口调用时就会报错。
然后我们在“src/main/java” Source Root下创建package “com.zhuoli.service.spring.mvc.demo”,并添加HelloController:
然后,我们在“src/main/resources”classPath中添加上篇文章中提到的两个配置文件,spring.xml和Spring-mvc.xml:
spring.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--启动DI管理-->
<context:annotation-config/>
<!-- 在父容器中不扫描@Controller注解,在子容器中只扫描@Controller注解 -->
<!--把控制器从包扫描中排除出去-->
<context:component-scan base-package="com.zhuoli.service.spring.mvc.demo">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
spring-mvc.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 启用Spring基于annotation的DI, 使用户可以在Spring MVC中使用Spring的强大功能。
激活 @Required, @Autowired,JSR 250's @PostConstruct, @PreDestroy and @Resource 等标注 -->
<context:annotation-config/>
<!--包扫描-->
<!--DispatcherServlet上下文,只管理@Controller的bean,忽略其他类型的bean, 例如@Service-->
<context:component-scan base-package="com.zhuoli.service.spring.mvc.demo">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 扩充了注解驱动,可以将请求参数绑定到控制器参数 -->
<!-- 简化配置:
(1)自动注册DefaultAnootationHandlerMapping,AnotationMethodHandlerAdapter
(2)提供一些列:数据绑定,数字和日期的format @NumberFormat, @DateTimeFormat, xml,json默认读写支持
-->
<mvc:annotation-driven>
<!--解决以@ResponseBody直接返回字符串时中文乱码问题-->
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!--前缀加上后缀生成一个完整的jsp路径-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
最后我们需要配置一下“web/WEB-INF/”下的web.xml,并创建jsp文件:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Archetype Created Web Application</display-name>
<!--指定Spring的配置文件地址-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>
<!--Tomcat Context生命周期监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--配置Spring MVC 的DispatcherServlet,指定配置文件的路径,拦截所有的请求-->
<servlet>
<!--这个名称如果不特别指定的话,跟配置文件名称有关联。如果特别指定配置文件了,则此名称就无所谓了-->
<servlet-name>springMvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!--contextConfigLocation这个参数可以不配置,如果不配置的话,那么默认的value就是/WEB-INF/[servlet名字]-servlet.xml-->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!--表示启动容器时初始化该Servlet-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMvc</servlet-name>
<!--DispatcherServlet拦截所有的请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
到这里,我们的应用代码就写完了,接下来就是通过Idea部署我们的Spring MVC应用了。
1.3 使用Idea部署Spring MVC应用
这里我们配置了本地的Tomcat地址后发现下方还有个Warning提醒,”No artifacts marked for deployment”,意思是说我们还没有标记部署的项目,点击右面的fix按钮,Idea会自动帮我们修复该问题,然后发现Warning就消失了。
上图点击fix后,会自动跳转到该图,这里的Application context跟我们最终访问应用的uri有关,比如这里配置为”/spring_mvc_demo_war_exploded”,那么最终访问的路径就是“http://localhost:8080/spring_mvc_demo_war_exploded/demo/rest/hello”。通过之前的文章我们也可以知道,这里的这个Application context配置肯定跟Tomcat配置文件的<context>节点的path属性有关。这里为了方便我们把配置改为”/”,直接通过”http://localhost:8080/demo/rest/hello”就能访问接口。
然后点击确定,我们的Tomcat Server就配置好了。点击运行
理论上就会默认开启一个浏览器tab,并访问链接”http://localhost:8080/”,但是发现浏览器tab确迟迟未打开,查看tomcat启动日志,发现报了ClassNotFound异常,如下:
后来解决方案如下:
“Put into Output Root”,其实是将应用以来的jar添加到WEB-INF\classes\lib目录下,这里不知道为什么不会帮我们自动添加(大部分ClassNotFound问题都是这个原因导致的,通过这种方式就能解决)。之后重新运行项目,发现浏览器自动访问了我们的默认路径”http://localhost:8080″:
2. Idea部署Spring MVC应用原理
2.1 手动启动Tomcat应用
我们本机安装了Tomcat的,如果我们要启动Tomcat,一般会到Tomcat的安装目录(“CATALINA_HOME”)下的bin目录,运行”./catalina.sh start”,就可以启动了。然后我们访问”http://localhost:8080″,就能看到那只熟悉的猫:
停止Tomcat也比较简单,直接在bin目录下执行脚本”./catalina.sh stop”即可。或者通过brew安装的,也可以直接运行以下命令:
brew services start tomcat
brew services stop tomcat
2.2 本地tomcat运行多实例
如果我们要在自己的电脑上运行多个tomcat实例,需要怎么做。一般有两种方式:
- 将整个Tomcat目录重新复制一份,每个tomcat实例之间完全独立
- 只将CATALINA_HOME目录下的conf目录拷贝一份到新目录(新目录就是CATALINA_BASE),多个实例共用CATALINA_HOME目录下的bin、lib文件夹
这里以本地安装的Tomcat目录为”/usr/local/Cellar/tomcat/9.0.37/libexec”为例,分别通过如上两种方式创建tomcat实例。
2.2.1 完全复制
首先我们在创建一个文件夹,用于存储tomcat实例:
cd ~
mkdir tomcat_instance
然后创建一个目录,用于存放我们从本地CATALINA_HOME中拷贝过去的Tomcat信息:
cd tomcat_instance
mkdir tomcat0
cd tomcat0
cp -a /usr/local/Cellar/tomcat/9.0.41/libexec/* ./
也就是说我们将CATALINA_HOME目录下的全部数据都拷贝到了”~/tomcat_instance/tomcat0″目录下,那么直接通过该目录下bin中的脚本启动的话,”~/tomcat_instance/tomcat0″其实就是一个CATALINA_HOME。我们可以通过这种方式,在一台机器中启动很多个tomcat,各个tomcat实例之间完全独立,有各自的CATALINA_HOME和CATALINA_BASE(有一点需要注意,同时启动多个时,需要修改server.xml中Connector的端口配置,不然会出现端口冲突)。
cd ~/tomcat_instance/tomcat0/bin
./catalina.sh start
可以看到启动日志:
2.2.2 多实例共用CATALINA_HOME
多实例共用CATALINA_HOME,其实就是共用CATALINA_HOME目录下的bin和lib文件夹,通过这种方式启动的多个tomcat实例,CATALINA_HOME是一样的,但是CATALINA_BASE各不相同。
cd ~/tomcat_instance
mkdir tomcat1
cd tomcat1
将我们Tomcat安装目录中的conf文件夹直接拷贝过来,并创建一些本tomcat实例工作需要的一些新文件夹:
cp -a /usr/local/Cellar/tomcat/9.0.41/libexec/conf ./
mkdir logs webapps work temp
修改conf目录下的server.xml,主要修改如下内容,如下图所示:
接着我们在webapps目录创建我们当前tomcat实例的应用:
cd ~/tomcat_instance/tomcat1/webapps
mkdir ROOT
cd ROOT
vi index.jsp
# index.jsp保存以下内容
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>My Tomcat1</title>
</head>
<body>
hello zhuoli 111
</body>
</html>
cd ~/tomcat_instance/tomcat1/webapps
mkdir myapp
cd myapp
vi index.jsp
# index.jsp保存以下内容
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>My Tomcat1</title>
</head>
<body>
hello zhuoli 222
</body>
</html>
在”~/tomcat_instance/tomcat1″工作目录下,创建启动暂停脚本:
cd ~/tomcat_instance/tomcat1
vi start_tomcat1.sh
# 保存以下内容
#!/bin/bash
export CATALINA_HOME=/usr/local/Cellar/tomcat/9.0.41/libexec
export CATALINA_BASE=/Users/chenhao/tomcat_instance/tomcat1
cd ${CATALINA_HOME}
./bin/catalina.sh start
cd ~/tomcat_instance/tomcat1
vi stop_tomcat1.sh
# 保存以下内容
#!/bin/bash
export CATALINA_HOME=/usr/local/Cellar/tomcat/9.0.41/libexec
export CATALINA_BASE=/Users/chenhao/tomcat_instance/tomcat1
cd ${CATALINA_HOME}
./bin/catalina.sh stop
赋予如上两脚本执行权限:
chmod 777 start_tomcat1.sh stop_tomcat1.sh
其实脚本也很简单,其实就是指定CATALINA_BASE,然后到CATALINA_HOME的bin目录下执行catalina.sh脚本。
执行start_tomcat.sh,日志如下:
2.2.3 Tomcat虚拟目录
上面的例子,我们都是将运行的应用直接放在CATALINA_BASE的webapps目录下,那有没有什么办法让我们把应用放在其它地方,但是依然可以通过Tomcat访问我们的应用,答案就是使用Tomcat虚拟目录。
比如我们在桌面上创建一个myapp1文件夹,文件夹下放一个jsp文件index.jsp。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>My Tomcat1</title>
</head>
<body>
hello zhuoli virtual index
</body>
</html>
然后我们在我们的CATALINA_BASE/conf/Catalina/localhost文件夹下创建一个xml文件ROOT.xml。
<?xml version="1.0" encoding="UTF-8"?>
<Context docBase="/Users/chenhao/Desktop/myapp1"/>
然后通过start_tomcat.sh脚本启动tomcat,之后访问”http://localhost:8080/”
可以发现这里项目的默认访问的页面不是/CATALINA_BASE/webapps/ROOT/index.jsp了,而是这里ROOT.xml中配置的”/Users/chenhao/Desktop/myapp1/index.jsp”。也就是说我们的虚拟目录配置生效了(所以Tomcat虚拟目录的作用其实是允许tomcat实例访问非webapps目录下的应用)。
如果我们在桌面上再起一个应用myapp2,文件夹下放一个jsp文件index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>My Tomcat1</title>
</head>
<body>
hello zhuoli virtual index 111
</body>
</html>
并且我们希望可以通过”http://localhost:8080/myapp2″访问到它,那么虚拟目录该如何配置?我们可以在CATALINA_BASE/conf/Catalina/localhost文件夹下再创建一个xml文件myapp2.xml。
<?xml version="1.0" encoding="UTF-8"?>
<Context docBase="/Users/chenhao/Desktop/myapp2"/>
关于Tomcat虚拟目录,需要注意以下几点:
- /conf/Catalina/localhost目录可以配置多个虚拟目录
- /conf/Catalina/localhost配置了ROOT.xml,将覆盖server.xml Host节点配置的默认目录 /webapps/ROOT
- /conf/Catalina/localhost文件夹下的配置文件 **.xml,中Context配置path = “abc”无效,默认为空
- /conf/Catalina/localhost文件夹下的配置文件 **.xml文件名即为uri路径,如果为多级路径,文件名使用#隔开,比如我们xml文件的名字为myapp1#myapp2.xml,那么我们配置的应用对应的uri为”\myap1\myapp2\**”
3. Idea部署Spring MVC应用原理
搞清楚本地的Tomcat如何启动的之后,我们来看一下Idea是如何将Spring MVC应用部署到Tomcat容器的。
3.1 Idea Project Structure
我们首先来看一下Idea的Project Structure,这个设置跟我们的最终的应用部署密切相关。
我们来简单过一下Project Structure的这些配置。
3.1.1 Project
这里的编译输出目录就是我们的web应用编译后的输出,比如上面的示例,编译后输出的结果如下:
3.1.1.2 Modules
Modules主要是为了我们方便管理项目的Module,上面的示例只有一个Module,如果我们的项目内部做了分层,比如Dao层一个Module,Service层一个Module,Controller层一个Module,这里就会出现多个目录。
一般Modules这里,保持默认设置即可。另外我们还可以选定某个Module,查看Module的配置信息,主要包括Sources、Paths、Dependencies三项。
Sources配置主要用于显示项目的目录资源,不同颜色代表不同的类型。
Paths配置主要用于设置Module的编译输出目录,这里的编译输出目录仅表示该Module的java文件的编译输出目录,跟上面3.1.1节Project的Project compiler output不同,Project compiler output设置的是整个应用的编译输出目录,不仅仅是java文件编译后对应的classes,还有一些web资源,项目依赖的lib。
Dependencies主要用于展示项目的依赖信息。
选中某个Module,在该模块同时会展示该Module中使用的框架信息。当我们选中Module下的Web框架时,可以展示Module下Web框架的一些配置信息。
可以看到这里显示了我们的Web资源的配置目录。
3.1.1.3 Libraries
这里主要用来展示和管理项目所以来的jar包,也可以对jar进行分组。
3.1.1.4 Facets
Facets,主要适用于展示一些项目中用到的框架的配置信息,官方的解释是:
When you select a framework (a facet) in the element selector pane, the settings for the framework are shown in the right-hand part of the dialog. (当你在左边选择面板点击某个技术框架,右边将会显示这个框架的一些设置)
3.1.1.5 Artifacts
官方给出的说明是:
An artifact is an assembly of your project assets that you put together to test, deploy or distribute your software solution or its part. Examples are a collection of compiled Java classes or a Java application packaged in a Java archive, a Web application as a directory structure or a Web application archive, etc.
(Artifact是编译后的Java类,Web资源等的整合,用以测试、部署等工作。再白话一点,就是说某个Module要如何打包,例如war exploded、war、jar、ear等等打包形式。某个Module有了Artifact就可以部署到应用服务器中了。
这里我们需要关注一下Output directory,我们的测试应用值为:
/Users/chenhao/Documents/IdeaProject/spring-mvc-demo/out/artifacts/spring_mvc_demo_war_exploded
这个目录就是我们要部署到Tomcat容器的web应用目录。
3.2 Idea启动Tomcat容器
我们点击启动配置好的web应用,可以看到如下启动日志:
这里我们看到了熟悉的CATALINA_BASE和CATALINA_HOME,其中CATALINA_HOME就是我们在配置1.3节中指定的(也是我们本机Tomcat的安装目录)。但是CATALINA_BASE是一个陌生的地址,那么会不会跟我们上面介绍的Tomcat本地运行多实例一样,Idea帮我们生成了一个实例,我们到CATALINA_BASE目录下去一探究竟:
只有conf、logs和work文件夹,这确实就是一个基于我们指定的CATALINA_BASE创建的一个Tomcat实例。但是可以发现,并没有webapps文件夹,那么我们的web应用时如何运行的,根据我们上面的介绍,毋庸置疑肯定是通过虚拟目录配置的。我们到CATALINA_BASE/conf/Catalina/localhost目录下去看以下配置文件,可以发现该文件夹下有一个ROOT.xml配置文件:
<Context path="" docBase="/Users/chenhao/Documents/IdeaProject/spring-mvc-demo/out/artifacts/spring_mvc_demo_war_exploded" />
docBase指定的就是我们上面Artifacts指定的Output directory,也就是我们的Web应用输出目录。
到这里我们可以明白,Idea是如何部署我们的Spring MVC应用的了,就是通过Tomcat多实例和虚拟目录实现的,当我们点击运行Tomcat容器后:
- 创建Artifacts中指定的Web输出目录
- 将web资源的根目录(3.1.1.2 Modules,选中Web框架时可以设置)下的所有文件(jsp、WEB-INF等)拷贝到Artifacts指定的Web输出目录下
- 将项目编译输出目录下的classes目录(3.1.1.2 Modules,选中Module,Paths配置中可设置)拷贝到Artifacts指定的Web输出目录下的WEB-INF下
- 将项目依赖的Libraries(3.1.1.3 Libraries节)拷贝到Artifacts指定的Web输出目录下的WEB-INF下
这样我们的Web应用的输出目录就配置好了,接下来就是创建Tomcat实例,并通过虚拟目录配置到我们的应用,这样就完成部署了。
其实我们生产上开发,很少有使用上述介绍的部署方法。一般我们会使用开箱即用的Spring Boot开发应用,并通过运维提供的自动化部署工具来部署应用。因为Spring Boot应用嵌入了Tomcat容器,所以部署时也不用像Spring MVC这样,关心应用如何跟外部的Tomcat容器做交互,如何启动应用。但正确理解Spring MVC应用是如何启动的,对于我们接下来介绍Spring MVC源码,也是有帮助的,同时也能更好地帮助我们理解Tomcat。
参考链接:
1. IntelliJ IDEA解决项目部署到Tomcat运行时提示jar包找不到问题