IDEA Java Web(Spring)项目从创建到打包(war)

创建Maven管理的Java Web应用

  • 创建新项目,"create new project",左侧类型选择"maven",右侧上方选择自己的SDK,点击"next"
  • “GroupId”类似与Java的package,此处为"com.seliote","ArtifactId"则为项目名,此处为"SpringDemo",接下来一路点击"next"直至创建工程完毕
  • 首次打开工程右下角会弹窗:"Maven projects need to be imported”,选择”Enable Auto-Import”,让其自动导入
  • 更改项目目录结构,"File" -> "Project Structure" -> "Project Settings" -> "Models" -> 点击左上角的加号(第二列设置上方或使用ALT+INSERT) -> 下滑找到Web并选择 -> 下方提示"'Web' Facet resources are not included in an artifact" -> 点击右侧"Create Artifact" -> OK,此时项目目录结构已经变成了标准的Web结构
  • 创建运行配置,右上方"Add Configuration..." -> 左上方加号(第一列设置上方或使用ALT+INSERT) -> 下滑找到”Tomcat Server” -> ”Local” -> 修改右侧上方name为Tomcat -> 下方提示“No artifacts marked for deployment” -> 点击右侧"Fix" -> 如果是最新版本的 IDEA,Run/Debug Configuration 中 Deploy 页默认的 Application Context 需要手动改成 /,否则会部署出错 -> 点击Tab页Server -> VM Options中填入-server,设置JVM启动参数模拟生产环境(区别于-client),防止因JVM优化出现bug -> OK,此时运行配置处已经显示Tomcat
  • 添加单元测试资源目录,SpringDemo/src/test文件夹上右键 -> New -> Directory -> 命名为resources -> (如果图标右下方有多条横线则跳过以下步骤)在新建的resources目录上右键 -> mark directory as -> test resources root,此时test文件夹内resources文件夹图标产生了变化
  • 修改一下默认的getter与setter代码模板,使其对于域的引用加上this.,创建一个Java源文件,代码空白处右键 -> Generate... -> Getter and Setter -> 点击 Getter Template 右侧菜单 -> 点击Intellij Default然后复制右侧所有代码 -> 点击左侧加号按钮 -> 输入名称Mine -> 右侧粘贴上刚才复制的代码 -> 将return $field.name;修改为return this.$field.name; -> OK,接下来修改setter,点击 Getter Template 右侧菜单 -> 点击Intellij Default然后复制右侧所有代码 -> 点击左侧加号按钮 -> 输入名称Mine -> 右侧粘贴上刚才复制的代码 -> $field.name = $paramName;修改为this.$field.name = $paramName; -> OK -> OK
  • 还有一点需要修改的是,运行程序会报 WARNING: Unknown version string [3.1]. Default version will be used.,虽然没什么影响,但是看着不舒服,修改 web.xml,将根节点中的 web-app version 修改为 3.1,xsi:schemaLocation 版本也修改为 3.1,因为 Tomcat 8 只支持到 3.1,如果是用的 Tomcat 9 则不会有这个警告
<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_3_1.xsd"
         version="3.1">
  • 项目至此创建完毕,目录结构如下图

编写Spring应用(纯Java方式)

  • 使用Log4j 2用于日志记录
    pom.xml中加入依赖
<!-- log4j 接口 -->
<dependency>
    <groupId>org.apache.logging.log4j</group
    <artifactId>log4j-api</artifactId>
    <version>2.11.1</version>
    <scope>compile</scope>
</dependency>
<!-- log4j 具体实现 -->
<dependency>
    <groupId>org.apache.logging.log4j</group
    <artifactId>log4j-core</artifactId>
    <version>2.11.1</version>
    <scope>runtime</scope>
</dependency>
<!-- 使 slf4j 使用 log4j 的桥接口 -->
<dependency>
    <groupId>org.apache.logging.log4j</group
    <artifactId>log4j-slf4j-impl</artifactId
    <version>2.11.1</version>
    <scope>runtime</scope>
</dependency>
<!-- commons logging 使用 log4j 的桥接口 -->
<dependency>
    <groupId>org.apache.logging.log4j</group
    <artifactId>log4j-jcl</artifactId>
    <version>2.11.1</version>
    <scope>runtime</scope>
</dependency>

编写单元测试的Log4j 2配置
SpringDemo/src/test/resources目录上右键new -> File,创建log4j2-test.xml文件

<?xml version="1.0" encoding="UTF-8"?>

<!-- 标准配置会影响单元测试日志行为,但单元测试配置更优先,所以需要配置单元测试的配置文件以避免标准配置的影响 -->
<configuration status="WARN">
    <!-- 创建Appender -->
    <appenders>
        <!-- 创建一个控制台Appender -->
        <Console name="Console" target="SYSTEM_OUT">
            <!-- 日志格式模板 -->
            <!-- 时间 线程(ID) 级别(颜色不同) logger名称 - 信息 -->
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
    </appenders>

    <!-- 创建Logger -->
    <loggers>
        <!-- 根logger级别是info -->
        <root level="info">
            <!-- 使用上面创建的Console Appender记录信息 -->
            <appender-ref ref="Console" />
        </root>
    </loggers>
</configuration>

编写标准Log4j 2配置
SpringDemo/src/main/resources目录上右键new -> File,创建log4j2.xml文件

<?xml version="1.0" encoding="UTF-8" ?>

<!-- 开启对于日志系统日志的记录 -->
<configuration status="WARN">
    <appenders>
        <!-- 创建一个控制台Appender -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
        <!-- 创建一个循环滚动日志Appender,将日志输出到指定目录下的application.log文件中,备份文件的文件名由filePattern 指定 -->
        <RollingFile name="FileAppender" fileName="/home/seliote/Temp/application.log" filePattern="../logs/application-%d{MM-dd-yyyy}-%i.log">
            <PatternLayout>
                <!-- 使用鱼标签,对属于相同请求的日志进行分组 -->
                <pattern>%d{HH:mm:ss.SSS} [%t] %X{%id} %X{username} %-5level %c{36} %l: %msg%n</pattern>
            </PatternLayout>
            <Policies>
                <!-- 每个日志文件大小不超过10MB -->
                <SizeBasedTriggeringPolicy size="10 MB" />
            </Policies>
            <!-- 保持不超过4个备份日志文件 -->
            <DefaultRolloverStrategy min="1" max="4" />
        </RollingFile>
    </appenders>

    <loggers>
        <!-- 根Logger级别是warn -->
        <root level="warn">
            <appender-ref ref="Console" />
        </root>
        <!-- 所有com.seliote中的logger级别都是info(子包默认也是)且是覆盖状态 -->
        <logger name="com.seliote" level="info" additivity="false">
            <!-- 同时使用两个Appender进行记录 -->
            <appender-ref ref="FileAppender" />
            <appender-ref ref="Console">
                <!-- 设置过滤器,该Logger虽然可以将日志记录到Console Appender,但只应用于包含了CONSOLE的Marker事件 -->
                <MarkerFilter marker="CONSOLE" onMatch="NEUTRAL" onMismatch="DENY" />
            </appender-ref>
        </logger>
        <!-- 确保报错出现在控制台 -->
        <logger name="org.apache" level="info" />
        <logger name="org.springframework" level="info" />
    </loggers>
</configuration>
  • 编写Spring MVC代码
    Spring框架的配置与启动全部采用Java方式
    引入依赖,pom.xml文件dependencies标签下加入
<!-- Tomcat 8 使用 3.1.0 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.1.3.RELEASE</version>
    <scope>compile</scope>
</dependency>
<!-- Spring 对象 XML 映射 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-oxm</artifactId>
    <version>5.1.3.RELEASE</version>
    <scope>compile</scope>
</dependency>
<!-- WebSocket 依赖 -->
<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
</dependency>
<!-- Spring websocket,主要使用了 SpringConfigurator 类 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>5.1.3.RELEASE</version>
    <scope>compile</scope>
</dependency>
<!-- 使用 Jackson 是因为 Spring 默认是使用 Jackson 进行 JSON 的处理与转换的
<!-- Jackson 底层 API 实现 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.8</version>
    <scope>compile</scope>
</dependency>
<!-- 标准的 Jackson 注解 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.9.8</version>
    <scope>compile</scope>
</dependency>
<!-- Jackson 对象绑定与序列化 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.8</version>
    <scope>compile</scope>
</dependency>
<!-- Jackson 扩展以支持 JSR-310 (Java 8 Date & Time API) -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.8</version>
    <scope>compile</scope>
</dependency>

首先编写配置与启动代码,右键 java 文件夹,创建类,输入 com.seliote.springdemo.config.Bootstrap 同时创建包与类

package com.seliote.springdemo.config;

import com.seliote.springdemo.filter.EncodingFilter;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import java.util.EnumSet;

/**
 * @author seliote
 * @date 2019-01-05
 * @description Spring Framework 启动类
 */
public class Bootstrap implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext aServletContext) {
        // 处理静态资源
        aServletContext.getServletRegistration("default")
                .addMapping("/resource/*", "*.jpeg", "*.png");

        // 注册并启动 Root Application Context,Web 的会自动调用 start()
        AnnotationConfigWebApplicationContext rootAnnotationConfigWebApplicationContext
                = new AnnotationConfigWebApplicationContext();
        rootAnnotationConfigWebApplicationContext.register(RootContextConfig.class);
        // Root Application Context 使用 ContextLoaderListener 启动
        aServletContext.addListener(new ContextLoaderListener(rootAnnotationConfigWebApplicationContext));

        AnnotationConfigWebApplicationContext servletAnnotationConfigWebApplicationContext
                = new AnnotationConfigWebApplicationContext();
        servletAnnotationConfigWebApplicationContext.register(ServletContextConfig.class);
        ServletRegistration.Dynamic servletRegistration = aServletContext.addServlet(
                "dispatcherServlet", new DispatcherServlet(servletAnnotationConfigWebApplicationContext));
        // 别忘了指示 Spring 启动时加载
        servletRegistration.setLoadOnStartup(1);
        servletRegistration.addMapping("/");

        // 注册监听器
        FilterRegistration.Dynamic filterRegistration =
                aServletContext.addFilter("encodingFilter", new EncodingFilter());
        filterRegistration.setAsyncSupported(true);
        filterRegistration.addMappingForUrlPatterns(
                EnumSet.allOf(DispatcherType.class),
                // 这个参数为 false 则该过滤器将在 web.xml 里注册的之前进行加载
                false,
                // 切记不能写成 /
                "/*"
        );
    }
}

下来写根应用上下文配置

package com.seliote.springdemo.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.seliote.springdemo.websocket.BroadcastWebSocket;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.stereotype.Controller;

import java.util.concurrent.Executor;

/**
 * @author seliote
 * @date 2019-01-05
 * @description Root Context 配置类
 */
@Configuration
@ComponentScan(
        basePackages = "com.seliote.springdemo",
        excludeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                value = {Configuration.class, Controller.class}
        )
)
// 启用异步,会创建出默认的配置
// proxyTargetClass 告诉 Spring 使用 CGLIB 而不是 Java 接口代理,这样才能创建接口未制定的异步与定时方法
@EnableAsync(proxyTargetClass = true)
// 启用定时任务,会创建出默认的配置
@EnableScheduling
// 实现 AsyncConfigurer, SchedulingConfigurer 以配置异步与定时任务
public class RootContextConfig implements AsyncConfigurer, SchedulingConfigurer {
    private static final Logger SCHEDULED_LOGGER =
            LogManager.getLogger(RootContextConfig.class.getName() + ".[SCHEDULED]");

    @Override
    public Executor getAsyncExecutor() {
        // 返回执行器,该类内部直接调用 @Bean 方法
        // Spring 将代理所有对 @Bean 方法的调用并缓存结果,确保所以该 Bean 不会实例化多次
        return threadPoolTaskScheduler();
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return null;
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar aScheduledTaskRegistrar) {
        // 设置定时任务的执行器
        aScheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler());
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        // 查找并注册所有扩展模块,如 JSR 310
        objectMapper.findAndRegisterModules();
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        // 不含时区的默认为 UTC 时间
        objectMapper.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);
        return objectMapper;
    }

    @Bean
    // Spring 有三种依赖注入方式,byType(类型注入),byName(名称注入),constructor(构造函数注入)
    // @Autowired 默认是按照类型注入的,@Resource 默认按名称注入
    // 如果类型注入时产生多义,则使用 @Qualifier 传入名称进行限定
    // 同时注入 marshaller 与 unmarshaller
    public Jaxb2Marshaller jaxb2Marshaller() {
        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
        // 指定扫描 XML 注解的包
        jaxb2Marshaller.setPackagesToScan("com.seliote.springdemo");
        return jaxb2Marshaller;
    }

    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(20);
        threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler - ");
        // 关闭前等待所有任务完成
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        // 等待关闭的最大时长
        threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
        threadPoolTaskScheduler.setErrorHandler(
                aThrowable -> SCHEDULED_LOGGER.warn("线程执行异常:" + aThrowable.getMessage())
        );
        threadPoolTaskScheduler.setRejectedExecutionHandler(
                (aRunnable, aThreadPoolExecutor) ->
                SCHEDULED_LOGGER.warn("执行任务 " + aRunnable.toString() + " 遭到拒绝 " + aThreadPoolExecutor.toString())
        );
        return threadPoolTaskScheduler;
    }

    // WebSocket 使用单例,注意,必须配置在 Root Application Context 中,否则会无效
    @Bean
    public BroadcastWebSocket broadcastWebSocket() {
        return new BroadcastWebSocket();
    }
}

Servlet 应用上下文配置

package com.seliote.springdemo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

/**
 * @author seliote
 * @date 2019-01-05
 * @description Servlet Context 配置类
 */
@Configuration
@EnableWebMvc
@ComponentScan(
        basePackages = "com.seliote.springdemo",
        // Include 的话 useDefaultFilters 需要设置为 false,否则 include 会无效
        // DefaultFilters 就是 include 所有 Component,且会和自己配置的进行合并,结果就是全部通过
        useDefaultFilters = false,
        includeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                value = Controller.class
        )
)
public class ServletContextConfig implements WebMvcConfigurer {

    // Jackson 用于 Json 实体的转换
    private ObjectMapper mObjectMapper;
    // Jackson 用于 XML 实体的转换
    private Marshaller mMarshaller;
    private Unmarshaller mUnmarshaller;

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> aHttpMessageConverters) {
        // 添加所有 Spring 会自动配置的转换器,顺序很重要,后面的往往有更宽的 MIME 类型会造成屏蔽
        aHttpMessageConverters.add(new ByteArrayHttpMessageConverter());

        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        // @ResponseBody 返回 String 时的默认编码,默认为 ISO-8859-1 会造成中文乱码
        stringHttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        aHttpMessageConverters.add(stringHttpMessageConverter);

        FormHttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter();
        formHttpMessageConverter.setCharset(StandardCharsets.UTF_8);
        aHttpMessageConverters.add(formHttpMessageConverter);

        aHttpMessageConverters.add(new SourceHttpMessageConverter<>());

        // 支持 XML 实体的转换
        MarshallingHttpMessageConverter marshallingHttpMessageConverter =
                new MarshallingHttpMessageConverter();
        marshallingHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(
                new MediaType("application", "xml"),
                new MediaType("text", "xml")
        ));
        marshallingHttpMessageConverter.setMarshaller(mMarshaller);
        marshallingHttpMessageConverter.setUnmarshaller(mUnmarshaller);
        marshallingHttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        // 配置完成后一定要记得添加
        aHttpMessageConverters.add(marshallingHttpMessageConverter);

        // 只要 Jackson Data Processor 2 在类路径上,就会创建一个默认无配置的 MappingJackson2HttpMessageConverter
        // 但是默认的只支持 application/json MIME
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter =
                new MappingJackson2HttpMessageConverter();
        mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(
                new MediaType("application", "json"),
                new MediaType("text", "json")
        ));
        mappingJackson2HttpMessageConverter.setObjectMapper(mObjectMapper);
        mappingJackson2HttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        aHttpMessageConverters.add(mappingJackson2HttpMessageConverter);
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer aContentNegotiationConfigurer) {
        aContentNegotiationConfigurer
                // 启用扩展名内容协商
                .favorPathExtension(true)
                // 只使用注册了的类型来解析扩展名 MediaType
                .useRegisteredExtensionsOnly(true)
                // 启用参数内容协商
                .favorParameter(true)
                // 设置参数内容协商名
                .parameterName("mediaType")
                // 启用 ACCEPT 请求头进行识别
                .ignoreAcceptHeader(false)
                // 默认 MediaType
                .defaultContentType(MediaType.APPLICATION_JSON)
                // 添加 XML 与 JSON 的支持
                .mediaType("xml", MediaType.APPLICATION_XML)
                .mediaType("json", MediaType.APPLICATION_JSON);
    }

    @Autowired
    public void setObjectMapper(ObjectMapper aObjectMapper) {
        mObjectMapper = aObjectMapper;
    }

    @Autowired
    public void setMarshaller(Marshaller aMarshaller) {
        mMarshaller = aMarshaller;
    }

    @Autowired
    public void setUnmarshaller(Unmarshaller aUnmarshaller) {
        mUnmarshaller = aUnmarshaller;
    }
}

编写控制器代码

package com.seliote.springdemo.controller;

import com.seliote.springdemo.pojo.User;
import com.seliote.springdemo.service.NotificationService;
import com.seliote.springdemo.service.UserService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author seliote
 * @date 2019-01-07
 * @description 用户操作相关控制器
 */
@Controller
@RequestMapping(value = "", method = {RequestMethod.GET})
public class UserController implements InitializingBean {
    private Logger mLogger = LogManager.getLogger();

    private UserService mUserService;
    private NotificationService mNotificationService;

    @Autowired
    public void setUserService(UserService aUserService) {
        mUserService = aUserService;
    }

    @Autowired
    public void setNotificationService(NotificationService aNotificationService) {
        mNotificationService = aNotificationService;
    }

    @Override
    public void afterPropertiesSet() {
        mLogger.info("UserController 初始化完成");
    }

    @ResponseBody
    @RequestMapping("add")
    public String addUser(@RequestParam(value = "name") String aUserName) {
        return "成功添加,ID: " + mUserService.addUser(aUserName);
    }

    @ResponseBody
    @RequestMapping("query/{userId:\d+}")
    public User queryUser(@PathVariable("userId") long aUserId) {
        // 调用异步方法
        mNotificationService.sendNotification(aUserId + "@selioteMail.com");
        return mUserService.queryUser(aUserId);
    }
}

设置编码的过滤器

package com.seliote.springdemo.filter;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @author seliote
 * @date 2019-01-05
 * @description 设置请求与响应的编码(这里无法设置 Spring 的)
 */
public class EncodingFilter implements Filter {
    @Override
    public void init(FilterConfig aFilterConfig) {
    }

    @Override
    public void doFilter(ServletRequest aServletRequest, ServletResponse aServletResponse, FilterChain aFilterChain)
            throws IOException, ServletException {
        aServletRequest.setCharacterEncoding(StandardCharsets.UTF_8.name());
        aServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
        aFilterChain.doFilter(aServletRequest, aServletResponse);
    }

    @Override
    public void destroy() {
    }
}

POJO

package com.seliote.springdemo.pojo;

import javax.xml.bind.annotation.XmlRootElement;

/**
 * @author seliote
 * @date 2019-01-07
 * @description 用户 POJO
 */
@SuppressWarnings("unused")
@XmlRootElement
public class User {
    private long mUserId;
    private String mUserName;

    // XmlRootElement 标注的类必须提供无参构造器
    public User() {
        mUserId = -1;
        mUserName = "未知";
    }

    public User(long aUserId, String aUserName) {
        mUserId = aUserId;
        mUserName = aUserName;
    }

    public long getUserId() {
        return mUserId;
    }

    public void setUserId(long aUserId) {
        mUserId = aUserId;
    }

    public String getUserName() {
        return mUserName;
    }

    public void setUserName(String aUserName) {
        mUserName = aUserName;
    }
}

服务,其中 NotificationService 模拟异步与定时任务

package com.seliote.springdemo.service;

import com.seliote.springdemo.pojo.User;

/**
 * @author seliote
 * @date 2019-01-07
 * @description 用户服务
 */
public interface UserService {
    default long addUser(String aName) {
        return -1;
    }

    default User queryUser(long aUserId) {
        return null;
    }
}
package com.seliote.springdemo.service;

import org.springframework.scheduling.annotation.Async;

/**
 * @author seliote
 * @date 2019-01-07
 * @description 通知相关服务
 */
public interface NotificationService {
    @Async
    default void sendNotification(String aEmailAddr) {
    }
}

仓库

package com.seliote.springdemo.repository;

import com.seliote.springdemo.pojo.User;

/**
 * @author seliote
 * @date 2019-01-07
 * @description 用户仓库
 */
public interface UserRepository {
    default long addUser(String aUserName) {
        return -1;
    }

    default User queryUser(long aUserId) {
        return null;
    }
}

服务与仓库的实现

package com.seliote.springdemo.impl.serviceimpl;

import com.seliote.springdemo.pojo.User;
import com.seliote.springdemo.repository.UserRepository;
import com.seliote.springdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author seliote
 * @date 2019-01-07
 * @description 用户服务实现
 */
@Service
public class UserServiceImpl implements UserService {

    private UserRepository mUserRepository;

    @Autowired
    public void setUserRepository(UserRepository aUserRepository) {
        mUserRepository = aUserRepository;
    }

    @Override
    public long addUser(String aUserName) {
        return mUserRepository.addUser(aUserName);
    }

    @Override
    public User queryUser(long aUserId) {
        return mUserRepository.queryUser(aUserId);
    }
}
package com.seliote.springdemo.impl.serviceimpl;

import com.seliote.springdemo.service.NotificationService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

/**
 * @author seliote
 * @date 2019-01-07
 * @description 通知服务实现
 */
@Service
public class NotificationServiceImpl implements NotificationService {
    private static final Logger LOGGER = LogManager.getLogger();

    @Async
    @Override
    public void sendNotification(String aEmailAddr) {
        LOGGER.info("开始发送通知");
        try {
            Thread.sleep(5_000L);
        } catch (InterruptedException exp) {
            exp.printStackTrace();
        }
        LOGGER.info("通知发送完成");
    }
}
package com.seliote.springdemo.impl.repositoryimpl;

import com.seliote.springdemo.pojo.User;
import com.seliote.springdemo.repository.UserRepository;
import org.springframework.stereotype.Repository;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author seliote
 * @date 2019-01-07
 * @description 用户仓库实现
 */
@Repository
public class UserRepositoryImpl implements UserRepository {
    private volatile long mUserIdCounter = 0;

    private final ConcurrentHashMap<Long, User> mUserMap = new ConcurrentHashMap<>();

    @Override
    public long addUser(String aUserName) {
        long userId = getNextUserId();
        mUserMap.put(userId, new User(userId, aUserName));
        return userId;
    }

    @Override
    public User queryUser(long aUserId) {
        if (mUserMap.containsKey(aUserId)) {
            User user = mUserMap.get(aUserId);
            return new User(user.getUserId(), user.getUserName());
        } else {
            return new User();
        }
    }

    private synchronized long getNextUserId() {
        return ++mUserIdCounter;
    }
}

最后再写一个 WebSocket,需要的话可以定义 SpringConfigurator 的子类来获取相关信息

package com.seliote.springdemo.websocket;

import com.seliote.springdemo.pojo.BroadcastMsg;
import com.seliote.springdemo.service.BroadcastWebSocketService;
import com.seliote.springdemo.websocket.coder.BroadcastMsgCoder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.socket.server.standard.SpringConfigurator;

import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.PongMessage;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

/**
 * @author seliote
 * @date 2019-01-08
 * @description 类似广播的 WebSocket
 */
@ServerEndpoint(
        value = "/time/{id}",
        // 设置编码与解码器,这样就能够支持响应的参数类型
        encoders = {BroadcastMsgCoder.class},
        decoders = {BroadcastMsgCoder.class},
        // 保证服务器终端的实例在所有事件或消息处理方法之前被正确注入与实例化
        configurator = SpringConfigurator.class
)
public class BroadcastWebSocket {

    private BroadcastWebSocketService mBroadcastWebSocketService;

    @Autowired
    public void setBroadcastWebSocketService(BroadcastWebSocketService aBroadcastWebSocketService) {
        mBroadcastWebSocketService = aBroadcastWebSocketService;
    }

    private final Logger mLogger = LogManager.getLogger();

    private static final String PING_MSG = "Pong me.";

    @OnOpen
    public void onOpen(Session aSession, @PathParam("id") String aId) {
        mLogger.info(" 创建连接,id:" + aId);
        mBroadcastWebSocketService.addSession(aSession);
    }

    // 如果不是单例 WebSocket 就无法使用 @Scheduled,而需要使用 Spring 提供的 TaskScheduler
    @Scheduled(initialDelay = 10_000L, fixedDelay = 10_000L)
    public void sendPing() {
        mLogger.info("PING PING PING");
        mBroadcastWebSocketService.sendPing(PING_MSG);
    }

    @OnMessage
    public void onPong(Session aSession, PongMessage aPongMessage) {
        mLogger.info(aSession + " PONG PONG PONG");
        mBroadcastWebSocketService.receivePong(aSession, aPongMessage);
    }

    @OnMessage
    public void onMsg(Session aSession, BroadcastMsg aBroadcastMsg) {
        // 打印内存地址,确定使用的单例
        mLogger.info(System.identityHashCode(this) + " - " + aSession + " 收到消息,内容:" + aBroadcastMsg);
        mBroadcastWebSocketService.sendMsgToAll(aBroadcastMsg);
    }

    @OnClose
    public void onClose(Session aSession, CloseReason aCloseReason) {
        mLogger.info(aSession + " 断开连接,原因:" + aCloseReason.getCloseCode() + " - " + aCloseReason.getReasonPhrase());
        mBroadcastWebSocketService.removeSession(aSession);
    }

    @OnError
    // @OnError 调用后 @OnClose 也会i调用
    public void onError(Session aSession, Throwable aThrowable) {
        mLogger.info(aSession + " 异常,原因:" + aThrowable.getMessage());
    }
}

编码与解码器

package com.seliote.springdemo.websocket.coder;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.seliote.springdemo.pojo.BroadcastMsg;

import javax.websocket.Decoder;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * @author seliote
 * @date 2019-01-09
 * @description BroadcastMsg 用于 WebSocket 的编码与解码器
 */
public class BroadcastMsgCoder implements Encoder.BinaryStream<BroadcastMsg>, Decoder.BinaryStream<BroadcastMsg> {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    static {
        OBJECT_MAPPER.findAndRegisterModules();
        OBJECT_MAPPER.configure(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT, false);
    }
    
    @Override
    public void init(EndpointConfig aEndpointConfig) {

    }

    @Override
    public void destroy() {

    }
    
    @Override
    public void encode(BroadcastMsg aBroadcastMsg, OutputStream aOutputStream) throws IOException {
        OBJECT_MAPPER.writeValue(aOutputStream, aBroadcastMsg);
    }

    @Override
    public BroadcastMsg decode(InputStream aInputStream) throws IOException {
        return OBJECT_MAPPER.readValue(aInputStream, BroadcastMsg.class);
    }
}

信息 Java bean

package com.seliote.springdemo.pojo;

import javax.xml.bind.annotation.XmlRootElement;

/**
 * @author seliote
 * @date 2019-01-09
 * @description 广播信息 POJO
 */
@SuppressWarnings("unused")
@XmlRootElement
public class BroadcastMsg {
    private String mSessionId;
    private String mTimestamp;
    private String mMsg;

    public BroadcastMsg() {}

    public BroadcastMsg(String aSessionId, String aTimestamp, String aMsg) {
        mSessionId = aSessionId;
        mTimestamp = aTimestamp;
        mMsg = aMsg;
    }

    public String getSessionId() {
        return mSessionId;
    }

    public void setSessionId(String aSessionId) {
        mSessionId = aSessionId;
    }

    public String getTimestamp() {
        return mTimestamp;
    }

    public void setTimestamp(String aTimestamp) {
        mTimestamp = aTimestamp;
    }

    public String getMsg() {
        return mMsg;
    }

    public void setMsg(String aMsg) {
        mMsg = aMsg;
    }

    @Override
    public String toString() {
        return mSessionId + " - " + mTimestamp + " - " + mMsg;
    }
}

服务与仓库

package com.seliote.springdemo.service;

import com.seliote.springdemo.pojo.BroadcastMsg;

import javax.websocket.EncodeException;
import javax.websocket.PongMessage;
import javax.websocket.Session;
import java.io.IOException;

/**
 * @author seliote
 * @date 2019-01-09
 * @description BroadcastWebSocket 的服务
 */
public interface BroadcastWebSocketService {

    default void addSession(Session aSession) {
    }

    default void sendPing(String aPingMsg) {
    }

    default void receivePong(Session aSession, PongMessage aPongMessage) {
    }

    default void sendMsg(Session aSession, BroadcastMsg aBroadcastMsg) throws IOException, EncodeException {
    }

    default void sendMsgToAll(BroadcastMsg aBroadcastMsg) {
    }

    default void removeSession(Session aSession) {
    }
}
package com.seliote.springdemo.repository;

import javax.websocket.Session;
import java.util.Set;

/**
 * @author seliote
 * @date 2019-01-09
 * @description BroadcastWebSocket 仓库
 */
public interface BroadcastWebSocketRepository {

    default void addSession(Session aSession) {
    }

    default Set<Session> getAllSession() {
        return null;
    }


    @SuppressWarnings("unused")
    default boolean getSessionState(Session aSession) {
        return false;
    }

    default void updateSessionState(Session aSession, boolean aState) {
    }

    default void removeSession(Session aSession) {
    }
}

服务与仓库的实现

package com.seliote.springdemo.impl.serviceimpl;

import com.seliote.springdemo.pojo.BroadcastMsg;
import com.seliote.springdemo.repository.BroadcastWebSocketRepository;
import com.seliote.springdemo.service.BroadcastWebSocketService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.websocket.EncodeException;
import javax.websocket.PongMessage;
import javax.websocket.Session;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

/**
 * @author seliote
 * @date 2019-01-09
 * @description BroadcastWebSocketService 实现
 */
@Service
public class BroadcastWebSocketServiceImpl implements BroadcastWebSocketService {
    private BroadcastWebSocketRepository mBroadcastWebSocketRepository;

    @Autowired
    public void setBroadcastWebSocketRepository(BroadcastWebSocketRepository aBroadcastWebSocketRepository) {
        mBroadcastWebSocketRepository = aBroadcastWebSocketRepository;
    }

    private final Logger mLogger = LogManager.getLogger();

    private String mPingMsg;

    @Override
    public void addSession(Session aSession) {
        mBroadcastWebSocketRepository.addSession(aSession);
    }

    @Override
    public void sendPing(String aPingMsg) {
        mPingMsg = aPingMsg;
        for (Session session : mBroadcastWebSocketRepository.getAllSession()) {
            mBroadcastWebSocketRepository.updateSessionState(session, false);
            try {
                session.getBasicRemote().sendPing(ByteBuffer.wrap(aPingMsg.getBytes(StandardCharsets.UTF_8)));
            } catch (IOException exp) {
                mLogger.warn(session + " 发送 PING 异常," + exp.getMessage());
            }
        }
    }

    @Override
    public void receivePong(Session aSession, PongMessage aPongMessage) {
        if (new String(aPongMessage.getApplicationData().array(), StandardCharsets.UTF_8).equals(mPingMsg)) {
            mBroadcastWebSocketRepository.updateSessionState(aSession, true);
        } else {
            mBroadcastWebSocketRepository.updateSessionState(aSession, false);
        }
    }

    @Override
    public void sendMsg(Session aSession, BroadcastMsg aBroadcastMsg) throws IOException, EncodeException {
        aSession.getBasicRemote().sendObject(aBroadcastMsg);
    }

    @Override
    public void sendMsgToAll(BroadcastMsg aBroadcastMsg) {
        for (Session session : mBroadcastWebSocketRepository.getAllSession()) {
            try {
                sendMsg(session, aBroadcastMsg);
            } catch (IOException | EncodeException exp) {
                mLogger.warn(session + " 发送信息异常," + exp.getMessage());
            }
        }
    }

    @Override
    public void removeSession(Session aSession) {
        mBroadcastWebSocketRepository.removeSession(aSession);
    }
}
package com.seliote.springdemo.impl.repositoryimpl;

import com.seliote.springdemo.repository.BroadcastWebSocketRepository;
import org.springframework.stereotype.Repository;

import javax.websocket.Session;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author seliote
 * @date 2019-01-09
 * @description BroadcastWebSocketRepository 实现
 */
@Repository
public class BroadcastWebSocketRepositoryImpl implements BroadcastWebSocketRepository {
    private final ConcurrentHashMap<Session, Boolean> mSessions = new ConcurrentHashMap<>();

    @Override
    public void addSession(Session aSession) {
        mSessions.put(aSession, true);
    }

    @Override
    public Set<Session> getAllSession() {
        Set<Session> sessions = new HashSet<>();
        mSessions.forEach((aSession, aBoolean) -> {
            if (aBoolean) {
                sessions.add(aSession);
            }
        });
        return sessions;
    }

    @Override
    public boolean getSessionState(Session aSession) {
        return mSessions.get(aSession);
    }

    @Override
    public void updateSessionState(Session aSession, boolean aState) {
        mSessions.put(aSession, aState);
    }

    @Override
    public void removeSession(Session aSession) {
        mSessions.remove(aSession);
    }
}

尝试运行,???404???(#黑人问号),给Bootstrap打断点发现并未执行代码
注释掉Bootstrap,尝试使用web.xml配置并启动Spring,编辑SpringDemo/web/WEB-INF/web.xml

<?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">

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.seliote.springdemo.config.RootContextConfig</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.seliote.springdemo.config.ServletContextConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
</web-app>

发现报
04-Nov-2018 19:26:26.062 SEVERE [RMI TCP Connection(2)-127.0.0.1] org.apache.catalina.core.StandardContext.startInternal One or more listeners failed to start. Full details will be found in the appropriate container log file
没什么有用的信息,根据指引再看Tomcat的报错
SEVERE [RMI TCP Connection(2)-127.0.0.1] org.apache.catalina.core.StandardContext.listenerStart Error configuring application listener of class org.springframework.web.context.ContextLoaderListener
java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener
问题很明显了,依赖出问题了
File -> Project Structure -> Artifacts -> SpringDemo:Web exploded -> Output Layout -> 展开Available Elements下的SpringDemo -> 选中SpringDemo下的所有依赖文件 -> 右键 -> Put into /WEB-INF/lib -> OK
再次尝试运行,正常,还原web.xml,同时配置一下session,编辑SpringDemo/web/WEB-INF/web.xml

<?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>SpringDemo</display-name>
    
    <!-- session失效时间为30分钟,仅允许http请求使用,仅使用cookie传送 -->
    <session-config>
        <session-timeout>30</session-timeout>
        <cookie-config>
            <http-only>true</http-only>
        </cookie-config>
        <tracking-mode>COOKIE</tracking-mode>
    </session-config>
    
    <!-- 开启集群间会话复制 -->
    <distributable />

</web-app>

反注释Bootstrap,尝试运行,熟悉的输出,再尝试访问http://localhost:8080/user?name=seliote与http://localhost:8080/123456进行测试

  • 创建单元测试
    引入依赖,pom.xml文件dependencies标签下加入
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

光标放在HelloWolrd类类名上CTRL + SHIFT + T -> create new test -> test library选择JUnit 4 -> 勾选需要测试的方法 -> OK,这将自动创建测试类UserControllerTest,键入代码

package com.seliote.springdemo.controller;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.Test;

public class UserControllerTest {

    @Test
    public void addUser() {
        Logger logger = LogManager.getLogger();
        logger.info("In HelloController test.");
    }
}

HelloControllerTest源代码左侧行号旁会有两个绿色的小三角,单击,然后选择"Run HelloControllerTest",这将运行单元测试,右上角运行配置处会自动变为HelloControllerTest,下次运行程序前记得切换运行配置为一开始新建的Tomcat

打包为WAR

需要明确一点,打包是 maven 的事,与其他无关,pom.xml根标签project下加入

<properties>
    <!-- 指示源文件编码 -->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- 不加这两行部分版本maven会报warning提示版本已不受支持 -->
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.targer>1.8</maven.compiler.targer>
</properties>

<!-- 指示 maven 打包为 war 文件而非 jar -->
<packaging>war</packaging>

<!-- build中所有的路径都是以src文件夹为根 -->
<build>
    <!-- java 源文件根目录 -->
    <sourceDirectory>src/main/java</sourceDirectory>
    <resources>
        <resource>
            <!-- 资源根目录 -->
            <directory>src/main/resources</directory>
        </resource>
    </resources>

    <!-- 单元测试 java 代码根目录 -->
    <testSourceDirectory>src/test/java</testSourceDirectory>
    <testResources>
        <testResource>
            <!-- 单元测试资源根目录 -->
            <directory>src/test/resources</directory>
        </testResource>
    </testResources>

    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>2.3</version>
            <configuration>
                <!-- web 根目录(包含 WEB-INF 的) -->
                <warSourceDirectory>web</warSourceDirectory>
            </configuration>
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

配置完毕,开始打包,IDEA中点击屏幕最右侧中间的Maven Projects(或鼠标移至左下角的选项图标,点击弹出的Maven Projects),单击展开SpringDemo,单击展开Lifecycle,双击 package进行打包,生成的war文件默认在target目录下
将生成的war文件(此处为SpringDemo-1.0-SNAPSHOT.war)复制到tomcat的webapp目录下,重启tomcat,浏览器进行访问(此处为http://localhost:8080/SpringDemo-1.0-SNAPSHOT/custom?name=seliote)测试

原文地址:https://www.cnblogs.com/seliote/p/9896385.html