創建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-appversion修改為 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)測試
