有兩種 Web
servlet 傳統的 MVC
reactive (webflux)
傳統的 MVC 用的是同步式 blocking IO,webflux 是使用非同步 non-blocking IO。目前大部分都還是使用 servlet
spring-boot-starter-web 可啟動 servlet web service,WebMvcAutoConfiguration 是自動設定類別
對 ContentNegotiatingViewResolver 及 BeanNameViewResolver 的 bean 註冊
支援靜態資源,含靜態網頁及 webjars
對 Converter, Generic Converter, Formatter 的 bean 自動註冊
支援 HttpMessageConverter 消息轉換器
支援 messageCodesResolver 自動註冊
自動使用 ConfigurableWebBindingInitializer 的 bean
embedded container
預設使用 tomcat
name | spring-boot |
---|---|
Tomcat | spring-boot-starter-tomcat |
Jetty | spring-boot-starter-jetty |
Undertow | spring-boot-starter-undertow |
設定
ref: Serve Static Resources with Spring | Baeldung
ref: Serving JSP with Spring Boot 3
可參考 ServerProperties (Spring Boot 3.3.5 API) 類別裡面的參數設定
tomcat 可透過 server.tomcat.*
設定
server:
port: 8081
servlet:
context-path: /demo1
只要實作 WebServerFactoryCustomizer 就能自訂 servlet container
CustomTomcatWebServerFactoryCustomizer.java
package tw.com.test.demo1.config;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class CustomTomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory server) {
server.addConnectorCustomizers((connector) -> {
connector.setPort(8088);
connector.setAsyncTimeout(Duration.ofSeconds(20).toMillis());
});
}
}
執行結果
Tomcat started on port 8088 (http) with context path '/demo1'
切換 container
假設要把 tomcat 換成 undertow,修改 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
啟動時,可看到換成了Undertow
[ main] o.s.b.w.e.undertow.UndertowWebServer : Undertow started on port 8081 (http) with context path '/demo1'
SSL
spring boot 可可定 ssl license,支援 https
自己產生 SSL 憑證
keytool -genkeypair -alias https -keyalg RSA -keysize 2048 -keystore keystore.jks -validity 36500
keytool -importkeystore -srckeystore keystore.jks -destkeystore keystore.p12 -deststoretype pkcs12
把 keystore.jks, keystore.p12 放到 resources 目錄
application.yml
server:
port: 8443
ssl:
protocol: TLS
# key-store-type={JKS|PKCS12}
key-store-type: PKCS12
key-store: classpath:keystore.p12
key-store-password: password
這樣就能瀏覽網頁 https://localhost:8443/
如果需要同時支援 http 及 https,就把 https 放在上面設定檔中。另外用程式的方式,啟動支援 http
package tw.com.test.demo1.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
@Configuration
public class ServerConfig {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
tomcat.addAdditionalTomcatConnectors(createStandardConnector());
return tomcat;
}
private Connector createStandardConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setPort(8080);
return connector;
}
}
啟動後會看到
Tomcat started on ports 8443 (https), 8080 (http) with context path '/demo1'
session persistence
server:
port: 8080
servlet:
context-path: /demo1
session:
persistent: true
store-dir: sessions
tracking-modes:
- cookie
- url
server 必須要用 kill -15 pid
不能用 kill -9 pid
刪除 process,否則不會觸發 session persistence
shutdown graceful
server:
shutdown: graceful
spring:
lifecycle:
# shutdown 的 timeout
timeout-per-shutdown-phase: 20s
spring boot 的 graceful shudown 也是呼叫 JDK 的 Runtime.getRuntime().addShutdownHook()
不建議用 kill -9 pid
的方式 kill process,一般應該使用 kill -15 pid
自訂 Web 設定
如果不想使用 spring boot 預設的 web configuration,可透過 WebMvcConfigurer
interface 自訂
ex: 增加 interceptor, 格式化器, 資源處理器
@Configuration
public class WebConfig implements WebMvcConfigurer {
}
註冊 interceptor
package tw.com.test.demo1.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor preHandle getRequestURI={}, getServletPath={}", request.getRequestURI(), request.getServletPath());
HttpSession session = request.getSession();
String userSession = (String) session.getAttribute("userSession");
if( userSession==null) {
// log.info("request.getContextPath()={}", request.getContextPath());
String contextPath = request.getContextPath();
response.sendRedirect(contextPath+"/index.html");
return false;
} else {
return true;
}
}
}
取得 http session 裡面的 "userSession" 判斷是否已經登入
WebConfig.java
package tw.com.test.demo1.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
// .addPathPatterns("/**")
.excludePathPatterns("/**/*.{jsp|html|css|js|png|jpg}")
.excludePathPatterns("/**/error")
.excludePathPatterns("/static/**");
}
}
application.yml
server:
shutdown: graceful
port: 8080
servlet:
context-path: /demo1
在 static 目錄加上測試檔案
/static/index.html
/static/js/test.js
/static/css/test.css
啟動後測試
http://localhost:8080/demo1/test 會轉址到 demo1/index.html
加上另一個 jsp 測試 http://localhost:8080/demo1/login.jsp 結果是下載檔案,而不是 render jsp,這邊要再瞭解看看原因
Message Converter
讓 Java Bean 可以自動轉換為 JSON/XML
spring boot 支援以下三種 JSON libary
Jackson 預設
JSON-B
Gson
如覺得 JacksonHttpMessageConvertersConfiguration 不符合要求,可覆蓋
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new StringWithoutSpaceDeserializer(String.class));
mapper.registerModule(module);
converter.setObjectMapper(mapper);
return converter;
}
}
StringWithoutSpaceDeserializer.java
package tw.com.test.demo1.handler;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
public class StringWithoutSpaceDeserializer extends StdDeserializer<String> {
private static final long serialVersionUID = -1L;
public StringWithoutSpaceDeserializer(Class<String> vc) {
super(vc);
}
@Override
public String deserialize(JsonParser p, DeserializationContext deserializationContext) throws IOException {
return StringUtils.trimToEmpty(p.getText());
}
}
這個 JSON 轉換器,設定 FAIL_ON_UNKNOWN_PROPERTIES
為 false。並配置一個 StringWithoutSpaceDeserializer,可過濾 http 參數首尾的空白
如果不用覆蓋的做法,可以增加自訂轉換器。也就是自訂 HttpMessageConverters
ref: # Day18 - HttpMessageConverter
類別轉換器
Converter 介面可以轉換參數的類別
package tw.com.test.demo1.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import tw.com.test.demo1.handler.CustomConverter;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new CustomConverter());
}
}
CustomerConverter.java
package tw.com.test.demo1.handler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.convert.converter.Converter;
@Slf4j
public class CustomConverter implements Converter<String, String> {
@Override
public String convert(String source) {
if (StringUtils.isNotEmpty(source)) {
source = source.trim();
}
return source;
}
}
註冊 Servlet, Filter, Listener
手動註冊
spring boot 有以下三種註冊類別
ServletRegistrationBean
FilterRegistrationBean
ServletListenerRegistrationBean
以下是 Servlet 的 sample
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Bean
public ServletRegistrationBean registerServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new RegisterServlet(), "/registerServlet");
servletRegistrationBean.addInitParameter("name", "username");
servletRegistrationBean.addInitParameter("sex", "male");
servletRegistrationBean.setIgnoreRegistrationFailure(true);
return servletRegistrationBean;
}
}
RegisterServelet.java
package tw.com.test.demo1.servlet;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class RegisterServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String name = getServletConfig().getInitParameter("name");
String sex = getServletConfig().getInitParameter("sex");
ServletOutputStream outputStream = resp.getOutputStream();
outputStream.println("name is " + name);
outputStream.println("sex is " + sex);
}
}
瀏覽網頁 http://localhost:8080/demo1/registerServlet 可看到結果
name is username
sex is male
自動掃描註冊
Servlet 3.0 以後不需要 web.xml,所有元件都可透過 annotation 註冊
@WebServlet
@WebFilter
@WebListener
因為 spring boot 內嵌的 tomcat 不會執行 ServletContainerInitializer interface。WebConfig 必須加上 @ServletComponentScan
,開啟掃描功能。
@Configuration
@RequiredArgsConstructor
@ServletComponentScan
public class WebConfig implements WebMvcConfigurer {
}
filter, servlet, listener 必須要在 WebConfig 的 sub package 裡面
JavaServlet.java
package tw.com.test.demo1.config.servlet;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "javaServlet", urlPatterns = "/javaServlet", asyncSupported = true, initParams = {@WebInitParam(name = "name", value = "javaServlet"), @WebInitParam(name = "sex", value = "male")})
public class JavaServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String name = getServletConfig().getInitParameter("name");
String sex = getServletConfig().getInitParameter("sex");
ServletOutputStream outputStream = resp.getOutputStream();
outputStream.println("name is " + name);
outputStream.println("sex is " + sex);
}
}
JavaFilter.java
package tw.com.test.demo1.config.servlet;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebInitParam;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@WebFilter(filterName = "javaFilter", urlPatterns = "/*", initParams = {@WebInitParam(name = "name", value = "javaFilter"), @WebInitParam(name = "code", value = "123456")})
@Slf4j
public class JavaFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String name = filterConfig.getInitParameter("name");
String code = filterConfig.getInitParameter("code");
log.info("JavaFilter init, name={}, code={}", name, code);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("JavaFilter doFilter");
chain.doFilter(request, response);
}
@Override
public void destroy() {
log.info("JavaFilter destroy");
}
}
ContextListener.java
package tw.com.test.demo1.config.listener;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@WebListener
public class ContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
log.info("application contextInitialized started");
}
@Override
public void contextDestroyed(ServletContextEvent event) {
log.info("application contextDestroyed stopped");
}
}
SessionListener.java
package tw.com.test.demo1.config.listener;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpSessionAttributeListener;
import jakarta.servlet.http.HttpSessionBindingEvent;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@WebListener("Session listener for the application")//description of the listener
public class SessionListener implements HttpSessionListener, HttpSessionAttributeListener {
@Override
public void attributeAdded(HttpSessionBindingEvent event) {
log.info("SessionListener attributeAdded event={}", event);
}
@Override
public void attributeRemoved(HttpSessionBindingEvent event) {
log.info("SessionListener attributeRemoved event={}", event);
}
@Override
public void attributeReplaced(HttpSessionBindingEvent event) {
log.info("SessionListener attributeReplaced event={}", event);
}
@Override
public void sessionCreated(HttpSessionEvent event) {
log.info("SessionListener sessionCreated event={}", event);
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
log.info("SessionListener sessionDestroyed event={}", event);
}
}
啟動後,瀏覽網頁 http://localhost:8080/demo1/javaServlet
2024-11-19T16:52:05.640+08:00 INFO 7277 --- [ main] tw.com.test.demo1.Demo1Application : Starting Demo1Application using Java 17 with PID 7277 (/Users/charley/project/idea/book/test/demo1/target/classes started by charley in /Users/charley/project/idea/book/test/demo1)
2024-11-19T16:52:05.653+08:00 INFO 7277 --- [ main] tw.com.test.demo1.Demo1Application : No active profile set, falling back to 1 default profile: "default"
2024-11-19T16:52:06.982+08:00 INFO 7277 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2024-11-19T16:52:06.998+08:00 INFO 7277 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-11-19T16:52:06.998+08:00 INFO 7277 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.31]
2024-11-19T16:52:07.081+08:00 INFO 7277 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/demo1] : Initializing Spring embedded WebApplicationContext
2024-11-19T16:52:07.081+08:00 INFO 7277 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1271 ms
2024-11-19T16:52:07.143+08:00 INFO 7277 --- [ main] t.c.t.d.config.listener.ContextListener : application contextInitialized started
2024-11-19T16:52:07.150+08:00 INFO 7277 --- [ main] t.c.t.demo1.config.servlet.JavaFilter : JavaFilter init, name=javaFilter, code=123456
2024-11-19T16:52:07.317+08:00 INFO 7277 --- [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2024-11-19T16:52:07.650+08:00 INFO 7277 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/demo1'
2024-11-19T16:52:07.665+08:00 INFO 7277 --- [ main] tw.com.test.demo1.Demo1Application : Started Demo1Application in 2.869 seconds (process running for 4.439)
2024-11-19T16:52:43.224+08:00 INFO 7277 --- [nio-8080-exec-1] t.c.t.demo1.config.servlet.JavaFilter : JavaFilter doFilter
2024-11-19T16:53:49.996+08:00 INFO 7277 --- [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
2024-11-19T16:53:50.007+08:00 INFO 7277 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
2024-11-19T16:53:50.016+08:00 INFO 7277 --- [ionShutdownHook] t.c.t.demo1.config.servlet.JavaFilter : JavaFilter destroy
2024-11-19T16:53:50.017+08:00 INFO 7277 --- [ionShutdownHook] t.c.t.d.config.listener.ContextListener : application contextDestroyed stopped
動態註冊
InitServlet.java
package tw.com.test.demo1.config.servlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class InitServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String name = getServletConfig().getInitParameter("name");
String sex = getServletConfig().getInitParameter("sex");
resp.getOutputStream().println("name is " + name);
resp.getOutputStream().println("sex is " + sex);
}
}
有兩種方法
- 用
@Bean
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Bean
public ServletContextInitializer servletContextInitializer() {
return (servletContext) -> {
ServletRegistration initServlet = servletContext.addServlet("initServlet", InitServlet.class);
initServlet.addMapping("/initServlet");
initServlet.setInitParameter("name", "initServlet");
initServlet.setInitParameter("sex", "male");
};
}
}
- 用
@Component
package tw.com.test.demo1.config;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.stereotype.Component;
import tw.com.test.demo1.config.servlet.InitServlet;
@Component
public class ServletConfig implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
ServletRegistration initServlet = servletContext.addServlet("initServlet2", InitServlet.class);
initServlet.addMapping("/initServlet2");
initServlet.setInitParameter("name", "initServlet2");
initServlet.setInitParameter("sex", "female");
}
}
兩種 servlet url 都是由 InitServlet 處理
http://localhost:8080/demo1/initServlet
http://localhost:8080/demo1/initServlet2
靜態資源處理
spring boot 預設會載入 classpath 以下目錄的資源
/static
/public
/resources
/META-INF/resources
可用這個參數修改
spring:
web:
resources:
static-locations: classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources
預設會把靜態資源映射到 /**
路徑,可以用以下方式修改
spring:
mvc:
static-path-pattern: "/pub/**"
spring MVC 預設使用 ResourceHttpRequestHandler 處理靜態資源,也可以實作自己的處理器。這邊增加了兩個靜態資源映射
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/assets/");
}
}