2025/04/07

SpringBoot3 Web 1

有兩種 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

啟動後測試


Message Converter

讓 Java Bean 可以自動轉換為 JSON/XML

spring boot 支援以下三種 JSON libary

如覺得 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

ref: # 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);

    }

}

有兩種方法

  1. @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");
        };
    }
}
  1. @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/");
    }
}

2025/03/31

SpringBoot3 - Spring Boot Log

spring 提供的 Logging :: Spring Boot 有以下幾種

name package
Java Util Logging spring-boot-starter-logging
Logback spring-boot-starter-logging
Log4j2 spring-boot-starter-log4j2

spring-boot-starter-logging 是預設引用的,所有 spring-boot-starter-* 都會引用 spring-boot-starter-logging。不需要自行引用。

spring-boot-starter-logging 使用 Commons Logging,支援 Java Util Logging、Log4j2、Logback

使用 starter 預設會使用 Logback,透過 logback 支援 Java Util Logging, Commons Logging, Log4J, or SLF4J。

Log Format

2024-11-15T17:25:40.765+08:00  INFO 4924 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
  • Date & Time

  • Log Level

  • Process ID

  • --- separator

  • [Application name] 如果有設定 spring.application.name 才會出現

  • [Thread name]

  • Correlation ID 如果有設定 tracing 才會出現

  • Logger name 通常是來源 class name 的縮寫

  • log message

Console Output

預設會出現 ERROR-level, WARN-level, and INFO-level messages

如果在 CLI 有加上 java -jar Demo1.jar --debug debug flag,就會出現 core loggers (embedded container, Hibernate, and Spring Boot) 更多的資訊。這邊不代表 application 的 DEBUG level message 會出現

CLI 也可以加上 --trace ,會增加 core logger 資訊


Color-coded Output

terminal 有支援 ANSI 時,就可以增加顏色

ex: %clr(%5p)

Level Color
FATAL Red
ERROR Red
WARN Yellow
INFO Green
DEBUG Green
TRACE Green

可以自訂顏色

  • blue

  • cyan

  • faint

  • green

  • magenta

  • red

  • yellow

ex: %clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){yellow}

File Output

spring boot 預設只會寫入 console log

要寫入 file log 必須設定 logging.file.namelogging.file.path,兩個都設定時,只會使用 logging.file.name。log file rotate 預設為 10M,log level 跟 console 一樣,會寫入 ERROR, WARN, INFO level

File Rotation

調整 log rotate 規則

要修改 application.properties(yaml),如果是 log4j 就要設定 log4j2.xml or log4j2-spring.xml

name desc
logging.logback.rollingpolicy.file-name-pattern filename pattern
logging.logback.rollingpolicy.clean-history-on-start 在 application 啟動時,要做 log cleanup
logging.logback.rollingpolicy.max-file-size logfile 的最大 size
logging.logback.rollingpolicy.total-size-cap log archives 刪除前的最大 size
logging.logback.rollingpolicy.max-history archive log files 保留幾個(default: 7)

Log Level

logging.level.<logger-name>=<level>

為 TRACE, DEBUG, INFO, WARN, ERROR, FATAL, or OFF 其中一個

logging.level.root 是設定 root logger

logging:
  level:
    root: "warn"
    org.springframework.web: "debug"
    org.hibernate: "error"

也可以設定環境變數,ex:

LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB=DEBUG

Log Groups

ex: 一次設定所有 tomcat 相關的 logger level,group 合併後,再設定 level

在 applicaiton.yaml

logging:
  group:
    tomcat: "org.apache.catalina,org.apache.coyote,org.apache.tomcat"

logging:
  level:
    tomcat: "trace"

spring pre-defined logging groups

name loggers
web org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
sql org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener

Log Shutdown hook

要在 application 關閉前,release logging resource,要在 JVM exits 時,加上 shutdown hook

使用 jar 時,shutdown hook 會自動註冊

如要關閉 shutdown hook

logging:
  register-shutdown-hook: false

Custom Log Configuration

logging.config 決定 logging framework,然後就能使用該 framework 的設定檔

Logging System Customization
Logback logback-spring.xmllogback-spring.groovylogback.xml, or logback.groovy
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties

建議使用 logback-spring.xml,不要用 logback.xml,spring 可能會無法控制 log 初始化

以下設定參數,可轉換為環境變數

Spring Environment System Property Comments
logging.exception-conversion-word LOG_EXCEPTION_CONVERSION_WORD The conversion word used when logging exceptions.
logging.file.name LOG_FILE If defined, it is used in the default log configuration.
logging.file.path LOG_PATH If defined, it is used in the default log configuration.
logging.pattern.console CONSOLE_LOG_PATTERN The log pattern to use on the console (stdout).
logging.pattern.dateformat LOG_DATEFORMAT_PATTERN Appender pattern for log date format.
logging.charset.console CONSOLE_LOG_CHARSET The charset to use for console logging.
logging.threshold.console CONSOLE_LOG_THRESHOLD The log level threshold to use for console logging.
logging.pattern.file FILE_LOG_PATTERN The log pattern to use in a file (if LOG_FILE is enabled).
logging.charset.file FILE_LOG_CHARSET The charset to use for file logging (if LOG_FILE is enabled).
logging.threshold.file FILE_LOG_THRESHOLD The log level threshold to use for file logging.
logging.pattern.level LOG_LEVEL_PATTERN The format to use when rendering the log level (default %5p).
PID PID

使用 logback,可多使用這些參數

Spring Environment System Property Comments
logging.logback.rollingpolicy.file-name-pattern LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN Pattern for rolled-over log file names (default ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz).
logging.logback.rollingpolicy.clean-history-on-start LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START Whether to clean the archive log files on startup.
logging.logback.rollingpolicy.max-file-size LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE Maximum log file size.
logging.logback.rollingpolicy.total-size-cap LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP Total size of log backups to be kept.
logging.logback.rollingpolicy.max-history LOGBACK_ROLLINGPOLICY_MAX_HISTORY

Logback Extension

logback-spring.xml 裡面有幾個 logback extensions

profile-specific cofiguration

根據 active profile,<springProfile> 可 include/exclude 部分設定

<springProfile name="staging">
    <!-- configuration to be enabled when the "staging" profile is active -->
</springProfile>

<springProfile name="dev | staging">
    <!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
</springProfile>

<springProfile name="!production">
    <!-- configuration to be enabled when the "production" profile is not active -->
</springProfile>

environment properties

<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host"
        defaultValue="localhost"/>
<appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender">
    <remoteHost>${fluentHost}</remoteHost>
    ...
</appender>

example

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

    <property name="LOG_PATH" value="/var/log/blog"/>
    <property name="LOG_FILENAME" value="spring-profiles"/>

    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <springProfile name="default,ide">
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>

    <springProfile name="test,prod">
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <encoder>
                <pattern>${FILE_LOG_PATTERN}</pattern>
            </encoder>
            <file>${LOG_PATH}/${LOG_FILENAME}.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>${LOG_PATH}/${LOG_FILENAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <maxHistory>7</maxHistory>
                <maxFileSize>100MB</maxFileSize>
                <totalSizeCap>1GB</totalSizeCap>
            </rollingPolicy>
        </appender>

        <root level="INFO">
            <appender-ref ref="FILE"/>
        </root>
    </springProfile>

</configuration>

Log4j2 Extension

可在 log4j2-spring.xml 設定使用

profile-specific configuration

<SpringProfile name="staging">
    <!-- configuration to be enabled when the "staging" profile is active -->
</SpringProfile>

<SpringProfile name="dev | staging">
    <!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
</SpringProfile>

<SpringProfile name="!production">
    <!-- configuration to be enabled when the "production" profile is not active -->
</SpringProfile> 

Environment Properties Lookup

<Properties>
    <Property name="applicationName">${spring:spring.application.name}</Property>
</Properties>

log4j2 system properties

ex: ConsoleAppender 在 windows 使用 Jansi

log4j2.skipJansi=false

2025/03/17

SpringBoot3 - Spring Boot 啟動過程

啟動入口

@SpringBootApplication
public class Demo1Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class);
    }

}

@SpringBootApplication 裡面就包含了

  • @SpringBootConfiguration

  • @EnableAutoConfiguration

    啟用應用的自動設定功能

  • @ComponentScan

    自動掃描 sub packages 裡面的 Spring Component


啟動 application

SpringApplication.run(Demo1Application.class);

也可以拆開

SpringApplication springApplication = new SpringApplication(Demo1Application.class);
springApplication.run(args);

也可以用 SpringApplicationBuilder

new SpringApplicationBuilder()
    .sources(Parent.class)
    .child(Demo1Application.class)
    .banerMode(Banner.Mode.OFF)
    .run(args);

jar 啟動

application 的 main 通常是在 console 啟動時被呼叫,如果打包成 jar,就會是另一種狀況

project 可用 mvn package 打包成 jar

|____org
| |____springframework
| | |____boot
| | | |____loader
| | | | |____ref
| | | | |____net
| | | | |____jarmode
| | | | |____launch
| | | | |____jar
| | | | |____zip
| | | | |____nio
| | | | |____log
|____META-INF
| |____MANIFEST.MF
| |____maven
| | |____tw.com.test
| | | |____demo1
| | | | |____pom.xml
| | | | |____pom.properties
| |____services
| | |____java.nio.file.spi.FileSystemProvider
|____BOOT-INF
| |____classes
| | |____tw
| | | |____com
| | | | |____test
| | | | | |____demo1
| | | | | | |____Demo1Application.class
| | |____application.yml
| |____layers.idx
| |____classpath.idx
| |____lib
  • BOOT-INF: 啟動 application 所需要的 classes

  • META-INF: application打包的相關描述檔案

  • org: spring boot 啟動所需要的引導類別


META-INF/MENIFEST.MF

Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.2
Build-Jdk-Spec: 17
Implementation-Title: demo1
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: tw.com.test.demo1.Demo1Application
Spring-Boot-Version: 3.3.5
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx

Main-Class: 啟動的引導類別

Start-Class: application 啟動類別


啟動的 log 可透過設定關閉

spring:
  main:
    log-startup-info: false

或是

SpringApplication springApplication = new SpringApplication(Demo1Application.class);
springApplication.setLogStartupInfo(false);
springApplication.run(args);

啟動失敗分析

由 FailureAnalyzer interface 處理

ex: PortInUseFailureAnalyzer 可處理重複使用 TCP Port 的 exception

可以自訂一個 PortInUseFailureAnalyzer

package tw.com.test.demo1;

import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.web.server.PortInUseException;

public class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {
    @Override
    protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
        return new FailureAnalysis(cause.getPort()+"PortInUse", "check "+cause.getPort(), cause);
    }
}

然後在 META-INF/spring.factories

org.springframework.boot.diagnostics.FailureAnalyzer=\
  tw.com.test.demo1.PortInUseFailureAnalyzer

也可以自訂自己的 applicaton exception,以及對應的 FailureAnalyzer

啟動 Banner

Text to ASCII Art Generator (TAAG) 這是將 text 轉換為 ascii 文字的網頁工具。

將要列印的 application 文字,轉換為 ascii 後,儲存到 /resources/banner.txt。啟動 application 時,就會看到 banner 已經換掉了。

以下是一些相關設定

# banner.txt encoding
spring.banner.charset=UTF-8
# banner.txt filepath
spring.banner.location=classpath:banner.txt
# console/log/off
spring.main.banner-mode=console

啟動事件與 listener

spring-boot-3.3.5.jar

META-INF/spring.factories 裡面註冊的 listener,這些都實作了 Spring 的 ApplicationListener interface

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

啟動事件的順序

  1. ApplicationStartingEvent

    spring application 啟動前發送

  2. ApplicationEnvironmentPreparedEvent

    已知要在 context 使用 spring 的 Environment 時,在 context 建立之前發送

  3. ApplicationContextInitializedEvent

    ApplicationContext 準備好了,ApplicationContextInitializer 已被呼叫,在 bean 載入前發送

  4. ApplicationPreparedEvent

    在 context refresh 前,且在 bean 定義被載入後發送

  5. ApplicationStartedEvent

    在 Spring context refresh 以後,在 application / CLI runner 呼叫前發送

  6. AvailabilityChangeEvent

    跟著上一個事件後發送,ReadinessState.CORRECT,代表 application 已處於活動狀態

  7. ApplicationReadyEvent

    在 application / CLI runner 呼叫後發送

  8. AvailabilityChangeEvent

    跟著上一個事件後發送,ReadinessState.ACCEPTING_TRAFFIC,代表 application 已經準備接收 request

  9. ApplicationFailedEvent

    在啟動異常時發送

以上只有 SpringApplication 發出的 SpringApplicationEvents 事件。

以下事件會在 ApplicationPreparedEvent 後,ApplicationStartedEvent 前發送

  1. WebServerInitializedEvent

    WebServer 啟動後發送。包含 ServletWebServerInitialziedEvent, ReactiveWebServerInitializedEvent

  2. ContextRefreshedEvent

    在 ApplicationContext refresh 後發送


自訂事件 listener

Demo1ApplicationListener.java

package tw.com.test.demo1;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class Demo1ApplicationListener implements ApplicationListener<AvailabilityChangeEvent> {
    @Override
    public void onApplicationEvent(AvailabilityChangeEvent event) {
        log.info("got event: {}, state: {}", event, event.getState());
        if( ReadinessState.ACCEPTING_TRAFFIC == event.getState() ) {
            log.info("applcation is started!");
        }
    }
}

啟動測試

2024-11-15T16:26:43.269+08:00  INFO 4595 --- [           main] t.c.test.demo1.Demo1ApplicationListener  : got event: org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5674e1f2, started on Fri Nov 15 16:26:40 CST 2024], state: CORRECT
2024-11-15T16:26:43.273+08:00  INFO 4595 --- [           main] t.c.test.demo1.Demo1ApplicationListener  : got event: org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5674e1f2, started on Fri Nov 15 16:26:40 CST 2024], state: ACCEPTING_TRAFFIC
2024-11-15T16:26:43.273+08:00  INFO 4595 --- [           main] t.c.test.demo1.Demo1ApplicationListener  : applcation is started!

Runner

在 application 啟動前,執行一些 code

  • ApplicationRunner

  • CommandLineRunner

@Component 或是 lambda 的 @Bean兩種寫法

package tw.com.test.demo1;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@SpringBootApplication
@Slf4j
public class Demo1Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class);
    }

    @Component
    public class Demo1ApplicationRunner implements ApplicationRunner {
        @Override
        public void run(ApplicationArguments args) throws Exception {
            log.info("ApplicationRunner runner args={}", args);
        }
    }
//    @Bean
//    public ApplicationRunner applicationRunner() {
//        return new Demo1ApplicationRunner();
//    }

    @Bean
    public ApplicationRunner applicationRunner2() {
        return (args) -> {
            log.info("CommandLineRunner2 args={}", args);
        };
    }

    @Bean
    public CommandLineRunner commandLineRunner() {
        return (args) -> {
            log.info("CommandLineRunner args={}", args);
        };
    }
}

執行結果

2024-11-15T16:57:46.247+08:00  INFO 4793 --- [           main] tw.com.test.demo1.Demo1Application       : CommandLineRunner args={}
2024-11-15T16:57:46.248+08:00  INFO 4793 --- [           main] tw.com.test.demo1.Demo1Application       : CommandLineRunner2 args=org.springframework.boot.DefaultApplicationArguments@d535a3d
2024-11-15T16:57:46.249+08:00  INFO 4793 --- [           main] tw.com.test.demo1.Demo1Application       : ApplicationRunner runner args=org.springframework.boot.DefaultApplicationArguments@d535a3d

2025/03/10

SpringBoot3 - Spring Boot Starter

starter 包含 自動設定及相關 library

官方都是以 spring-boot-starter-* 命名。第三方要以 *-spring-boot-starter 命名,ex: druid-spring-boot-starter

分類

application

name desc
spring-boot-starter 核心 starter,含自動設定、log 及支援 YAML
spring-boot-starter-amqp Spring AMQP, Rabbit MQ
spring-boot-starter-aop Spring AOP
spring-boot-starter-artemis Apache Artemis,支援 JMS 的 MQ
spring-boot-starter-batch Spring Batch
spring-boot-starter-cache Spring Cache
spring-boot-starter-data-cassandra Cassandra + Spring Data Cassandra
spring-boot-starter-data-cassandra-reactive Cassandra + Spring Data Cassandra Reactive
spring-boot-starter-data-couchbase Couchbase + Spring Data Couchbase
spring-boot-starter-data-couchbase-reactive Couchbase + Spring Data Couchbase Reactive
spring-boot-starter-data-elasticsearch Elasticsearch + Spring Data Elasticsearch
spring-boot-starter-data-jdbc Spring Data JDBC
spring-boot-starter-data-jpa Spring Data JPA + Hibernate
spring-boot-starter-data-ldap Spring Data LDAP
spring-boot-starter-data-mongodb MongoDB + Spring Data MongoDB
spring-boot-starter-data-mongodb-reactive MongoDB + Spring Data MongoDB Reactive
spring-boot-starter-data-neo4j Neo4J + Spring Data Neo4J
spring-boot-starter-data-r2dbc Spring Data R2DBC
spring-boot-starter-data-redis Redis + Spring Data Redis + Lettuce
spring-boot-starter-data-redis-reactive Redis + Spring Data Redis Reactive + Lettuce Client
spring-boot-starter-data-rest Spring Data REST + Spring Data repositories,輸出 REST
spring-boot-starter-freemarker 以 FreeMarker View 建立 Spring Web application
spring-boot-starter-graphql Spring GraphQL
spring-boot-starter-grovvy-templates Groovy View 建立 Spring Web application
spring-boot-starter-hateoas Spring MVC + Spring HATEOAS 建立 RESTful Web application
spring-boot-starter-integration Spring Integration
spring-boot-starter-jdbc JDBC + HikariCP connection pool
spring-boot-starter-jersey JAX-RS + Jersey 建立 RESTful Web application,可替代 spring-boot-starter-web
spring-boot-starter-jooq jOOQ 存取 SQL database。可替代 spring-boot-starter-data-jpa 或 spring-boot-starter-jdbc
spring-boot-starter-json 讀寫 JSON
spring-boot-starter-mail Java Mail + Spring Mail Sender
spring-boot-starter-mustache 以 Mustache view 建立 Web Application
spring-boot-starter-oauth2-client Spring Security's OAuth2/OpenID 客戶端連線
spring-boot-starter-oauth2-resource-server Spring Security's OAuth2 資源伺服器
spring-boot-starter-quartz Quartz
spring-boot-starter-rsocket RSocket client and server
spring-boot-starter-security Spring Security
spring-boot-starter-test JUnit Jupiter + Hamcrest + Mockito
spring-boot-starter-thymeleaf Thymeleaf View 建立 MVC web application
spring-boot-starter-validation Java Bean Validation + Hibernate Validator
spring-boot-starter-web Spring MVC 建立 RESTful Web application,以 Tomcat 為內嵌伺服器
spring-boot-starter-web-services Spring Web Services
spring-boot-starter-webflux Spring Reactive Web 建立 WebFlux application
spring-boot-starter-websocket Spring WebSocket

如果官方沒有的 starter,可使用第三方自制的 Spring Boot Starter,ex: Dubbo, ZooKeeper, MyBatis

production

name desc
spring-boot-starter-actuator Sprign Boot Actuator,正式環境的監控與應用管理

technical

可排除或替換預設的技術套件

name desc
spring-boot-starter-jetty 以 Jetty 為 servlet container,替代 spring-boot-starter-tomcat
spring-boot-starter-log4j2 log4j2,替代 spring-boot-starter-logging
spring-boot-starter-logging Logback
spring-boot-starter-reactor-netty 以 Netty 作為內嵌的 reactive http server
spring-boot-starter-tomcat 以 Tomcat 為內嵌的 servlet container,這是預設的,也被用在 spring-boot-starter-web
spring-boot-starter-undertow 以 Undertow 作為內嵌的 servlet container,可替代 spring-boot-starter-tomcat

ex: 使用 Jetty 替代 tomcat

修改pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- exclude tomcat -->
            <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-jetty</artifactId>
            <!-- <artifactId>spring-boot-starter-undertow</artifactId> -->
        </dependency>

自動設定

所有自動設定的類別都是由 spring-boot-autoconfigure 模組提供的

ref: # 深入理解自動配置原理之@SpringApplcation


MailSender

spring-boot-start-mail 提供了

  • org.springframework.mail.javamail.JavaMailSender

  • org.springframework.mail.javamail.JavaMailSenderImpl

另外有一個自動設定類別

  • org.springframework.boot.autoconfigure.mail.MailSnederAutoConfiguration

該類別被註冊到這個檔案裡面,檔案內容就是自動設定類別的字串

/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

application.yml

spring:
  mail:
    host: smtp.gmail.com
    username: service@larzio.com
    password: XXXXXX
    properties:
      "[mail.smtp.socketFactory.class]": javax.net.ssl.SSLSocketFactory
      "[mail.smtp.socketFactory.fallback]": false
      "[mail.smtp.socketFactory.port]": 465
      "[mail.smtp.connectiontimeout]": 5000
      "[mail.smtp.timeout]": 3000
      "[mail.smtp.writetimeout]": 5000

mail:
  from: service@test.com
  fromname: service
  bcc:
  subject: Spring Boot Mail Test

Demo1Application.java

package tw.com.test.demo1;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.bind.annotation.RestController;

@EnableConfigurationProperties({MailProperties.class})
@RequiredArgsConstructor
@SpringBootApplication
@RestController
public class Demo1Application {
    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class);
    }
}

MailProperties.java

package tw.com.test.demo1;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "mail")
public class MailProperties {
    private String from;
    private String fromname;
    private String bcc;
    private String subject;

}

EmailController.java

package tw.com.test.demo1;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;

@Slf4j
@RequiredArgsConstructor
@RestController
public class EmailController {
    private final JavaMailSender javaMailSender;
    private final MailProperties mailProperties;

    @RequestMapping("/sendSimpleEmail")
    @ResponseBody
    public boolean sendSimpleEmail(@RequestParam("email") String email, @RequestParam("text") String text) {
        try {
            SimpleMailMessage msg = createSimpleMsg(email, text);
            javaMailSender.send(msg);
        } catch (Exception ex) {
            log.error("Error:", ex);
            return false;
        }
        return true;
    }

    @RequestMapping("/sendMimeEmail")
    @ResponseBody
    public boolean sendMimeEmail(@RequestParam("email") String email, @RequestParam("text") String text) {
        try {
            MimeMessage msg = createMimeMsg(email, text, "java.png");
            javaMailSender.send(msg);
        } catch (Exception ex) {
            log.error("Error:", ex);
            return false;
        }
        return true;
    }

    /**
     * @param email
     * @param text
     * @param attachmentClassPathFilename
     * @return
     * @throws MessagingException
     * @throws UnsupportedEncodingException
     */
    private MimeMessage createMimeMsg(String email, String text, String attachmentClassPathFilename) throws MessagingException, UnsupportedEncodingException {
        MimeMessage msg = javaMailSender.createMimeMessage();
        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(msg, true);
        mimeMessageHelper.setFrom(mailProperties.getFrom(), mailProperties.getFromname());
        mimeMessageHelper.setTo(email);
        if (!mailProperties.getBcc().equals("")) {
            mimeMessageHelper.setBcc(mailProperties.getBcc());
        }
        mimeMessageHelper.setSubject(mailProperties.getSubject());
        mimeMessageHelper.setText(text);
        mimeMessageHelper.addAttachment(attachmentClassPathFilename, new ClassPathResource(attachmentClassPathFilename));
        return msg;
    }

    /**
     * @param email
     * @param text
     * @return
     */
    private SimpleMailMessage createSimpleMsg(String email, String text) {
        SimpleMailMessage msg = new SimpleMailMessage();
        msg.setFrom(mailProperties.getFrom());
        msg.setTo(email);
        if (!mailProperties.getBcc().equals("")) {
            msg.setBcc(mailProperties.getBcc());
        }
        msg.setSubject(mailProperties.getSubject());
        msg.setText(text);
        return msg;
    }

}

測試

http://localhost:8080/sendMimeEmail?email=charley@maxkit.com.tw&text=hello

http://localhost:8080/sendSimpleEmail?email=charley@maxkit.com.tw&text=hello

2025/03/03

SpringBoot3 Configuration

Spring Boot 3 的相關設定

設定類別

spring 3.0 以前,都是使用 XML 設定檔。3.0 以後,是透過 @Configuration 註解的類別做設定。

@SpringBootApplication 裡面包含了 @SpringBootConfiguration,該設定可代替 @Configuration

example: @Bean 就類似以前 xml 的 <bean>

@SpringBootConfiguration
public class MainConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

SprintBoot 可使用一個 @SpringBootConfiguration或是 @Configuration 類別做設定,但也可以區分不同功能的設定,然後再使用 @Import 彙整在一起。

@SpringBootConfiguration
@Import({Config1.class, Config2.class})
public class MainConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

但如果設定類別都在類別掃描路徑上,可以用 @ComponentScan 掃描所有 packages。因 @SpringBootConfiguration 包含了 @ComponentScan,Application 以下的所有 packages。


也可以用 @ImportResource 匯入 XML 設定檔

設定檔

Spring Boot 的主要設定檔在 application.properties

如果有使用 Spring Cloud,有另一個 bootstrap.properties,該設定檔的優先權比 application.properties 高。

  • bootstrap 由 ApplicationContext 載入,優先權高

  • bootstrap 的設定不能被覆寫

設定檔可以用 .properties 或是 .yml (yaml 格式)

server.port = 8090
server.servlet.contextPath = /test

可使用 @PropertySource 匯入

.yml

server:
  port: 8090
  servlet:
    contextPath: /test

可用 YamlPropertiesFactoryBean 轉換為 Properties,或是用 YamlMapFactoryBean 轉換為 Map。也可以用 YamlPropertySourceLoader 在入為 PropertySource


@ConfigurationProperties 支援兩種格式的設定檔


設定綁定

所有已載入 Spring 的設定,都可以用 Environment 取得

@Autowired
private Environment env;

//
String getProperties(String key);

使用 @Value

使用 @Value

  1. ${ property : default_value }

  2. #{ obj.property ?: default_value }

example:

application.properties

jdbc.driverClass=com.mysql

在類別中,可用以下方式取得

    @Value("${jdbc.driverClass}")
    private String driver;

也可以用 @PropertySource

@Data
@Component
@PropertySource(value={"/config/db.properties"})
public class DBProperties {
    @Value("${jdbc.driverClass}")
    private String driverClass;
}

映射到類別、Constructor

可直接以 @ConfigurationProperties 將設定映射到一個類別

ex:

application.yaml

demo1:
  name: test
  users:
    - user1
    - user2
  params:
    place : userplace

demo2:
  name: test2
  age: 11
  birthday: 2024/11/11 11:11:11

demo3:
  name: test3
  age: 22

Demo1.properties

package tw.com.test.demo1;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.List;
import java.util.Map;

@Data
@ConfigurationProperties(prefix="demo1")
public class Demo1Properties {
    private String name;
    private List<String> users;
    private Map<String, String> params;
}

Demo2Properties.java

package tw.com.test.demo1;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

@Data
@NoArgsConstructor
@ConfigurationProperties(prefix="demo2")
public class Demo2Properties {
    private String name;
    private int age;
    private Date birthday;

    @ConstructorBinding
    public Demo2Properties(String name,
                           @DefaultValue("1") int age,
//                           @DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss") Date birthday,
                           @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss", timezone="GMT+8") Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }
}

Demo1Application.java

package tw.com.test.demo1;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@RequiredArgsConstructor
@EnableConfigurationProperties(value = {Demo1Properties.class, Demo2Properties.class})
@Slf4j
public class Demo1Application {

    private final Demo1Properties demo1Properties;
    private final Demo2Properties demo2Properties;

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner() {
        return (args) -> {
            log.info("demo1 properties: {}", demo1Properties);
            log.info("demo2 properties: {}", demo2Properties);
        };
    }
}

CommandLineRunner 的用途是,在 CLI 啟動 application 後,會被呼叫的 method

執行結果

demo1 properties: Demo1Properties(name=test, users=[user1, user2], params={place=userplace})
demo2 properties: Demo2Properties(name=test2, age=11, birthday=Mon Nov 11 11:11:11 CST 2024)

Bean 綁定

@ConfigurationProperties 除了用在 class,也可以用在 @Bean

MainConfig.java

@SpringBootConfiguration
public class MainConfig {
    @Bean
    @ConfigurationProperties(prefix="demo3")
    public Demo3Properties demo3Properties() {
        return new Demo3Properties();
    }
}

Demo3Properties.java

package tw.com.test.demo1;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Demo3Properties {
    private String name;
    private int age;
}

Demo1Application 不需要寫在 @EnableConfigurationProperties

@SpringBootApplication
@RequiredArgsConstructor
@EnableConfigurationProperties(value = {Demo1Properties.class, Demo2Properties.class})
@Slf4j
public class Demo1Application {

    private final Demo1Properties demo1Properties;
    private final Demo2Properties demo2Properties;
    private final Demo3Properties demo3Properties;

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner() {
        return (args) -> {
            log.info("demo1 properties: {}", demo1Properties);
            log.info("demo2 properties: {}", demo2Properties);
            log.info("demo3 properties: {}", demo3Properties);
        };
    }
}

執行結果

demo3 properties: Demo3Properties(name=test3, age=22)

設定類別掃描

在 Application 加上 @ConfigurationPropertiesScan 可掃描該 package 以下的所有設定類別,如果要掃描特定package,就加上 basePackages

@SpringBootApplicaiton
@RequiredArgsConstructor
@ConfigurationPropertiesScan
public class Application {

}

設定驗證

使用 @ConfigurationProperties 可利用 JSR-303 javax.validation 驗證設定值

pom.xml 要加上套件

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>

在設定欄位上加上 @NotNull

@Data
@Validated
@NoArgsConstructor
@ConfigurationProperties(prefix="demo2")
public class Demo2Properties {
    @NotNull
    private String name;

    private int age;
    private Date birthday;

外部設定

設定來源

  • properties

  • YAML

  • 環境變數

  • CLI 參數

設定的優先等級

由低到高

  1. 預設參數 SpringApplication.setDefaultProperties

  2. @PropertySource 綁定的設定

  3. 套用 application 檔案的參數

  4. 設定了 random.* 的參數

  5. 系統環境變數

  6. Java System Properties

  7. java:comp/env 的 JNDI 參數

  8. ServletContext 初始化參數

  9. ServletConfig 初始化參數

  10. 來自 SPRING_APPLICATION_JSON 的參數

  11. CLI 參數

  12. 單元測試裡面的參數

  13. 使用 @TestPropertySource 綁定的設定

  14. Devtools 全域設定參數,來自 $HOME/.config/spring-boot

多個設定檔案的優先等級,低到高

  1. jar 裡面的設定檔

  2. 指定了 profile 的設定檔,ex: application-dev.properties (jar 裡面)

  3. application 設定檔 (jar 外面)

  4. 指定了 profile 的設定檔,ex: application-dev.properties (jar 外面)


CLI 參數,是啟動時,用 -- 開頭的參數

ex:

java -jar application.jar --server.port=8090

用 maven 啟動

mvn spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=8090'

如果不希望把CLI 參數增加到 spring 環境中,可以禁用

public static void main(String[] args) {
    SpringApplication.setAddCommandLineProperties(false);
    SpringApplication.run(Application.class, args);
}

匯入設定檔

可利用 spring.config.import 指定匯入設定檔的路徑

spring:
  config:
    import:
      - optional:classpath:/config/app.yml

random

RandomVlauePropertySource 可用來注入亂數值,可以產生 int, long, uuid, string

ex:

demo1:
  age: ${random.int[10,100]}
  security:
    security-key: ${random.value}
    security-code: ${random.uuid}

多個設定環境

有可能遇到 正式、測試、開發環境不同的設定

yaml 用 --- 區隔

ex:

spring:
  application:
    name: "Demo1"
---
spring:
  application:
    name: "Production1"
  config:
    activate:
      on-cloud-platform: "kubernates"

properties

spring.application.name=Demo1
#---
spring.application.name=Production1
config.activate.on-cloud-platform=kubernates

以下設定檔,可限制只在 dev 或是 test 環境使用

  • spring.config.activate.on-profile

  • spring.config.activate.on-cloud-platform

spring:
  profiles:
    active: dev
---
spring:
  config:
    activate:
      on-profile: "dev|test"

Profile

profile可以分離設定

如果不指定時,就會使用預設的 Profiledefault,可以修改預設 profile

spring:
  profiles:
    default: dev

啟用 profile

啟用 dev, test

spring:
  profiles:
    active: dev, test

也可以在程式裡面啟用

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(Demo1Application.class);
        springApplication.setAdditionalProfiles("dev", "test");
        springApplication.run(args);
    }

profile 可啟用設定跟 bean,可用在 @Component, @Configuration, @ConfigurationProperties

ex: 以下設定只有在啟用 "main" 時,才會載入這個設定

@Profile("main")
@SpringBootConfiguration
public class MainConfig {
}

yaml

spring:
  profiles:
    default: dev
    active: dev, main

@Profile 用在 @ConfigurationProperties 時要注意

  • 如果是用掃描的註冊方式,@Profile 可直接用在 @ConfigurationProperties

  • 如果是用 @EditConfigurationProperties,則要把 @Profile 用在 @EditConfigurationProperties 設定類別上


切換 Profile

spring.profiles.active 設定啟用的 profile

但還是能在 CLI 參數調整,因為 CLI 參數優先權比設定檔高

java -jar application.jar --spring.profiles.active=test
mvn spring-boot:run -Dapp.profiles=test

如果不希望被替代,可改用 include

spring:
  profiles:
    default: dev
    active: dev, main
    # include 的 profile 不會被覆蓋
    include:
      - dev
      - main

profile 分組

在 yaml 將 profile 分組,當 main 啟用時, main1, main2 也會被啟用

spring:
  profiles:
    default: dev
    active: dev, main
    # include 的 profile 不會被覆蓋
    include:
      - dev
      - main
    group:
      main:
        - main1
        - main2
spring:
  config:
    active:
      on-profile: main1
---
spring:
  config:
    active:
      on-profile: main2

指定 profile 的設定檔

application-${profile}

ex:

application.yml

application-dev.yml

application-test.yml

application-main.yml

active: dev, main優先順序 default > dev > main


使用限制

  • application.profiles.default

    不指定 profile 的預設 profile

  • application.profiles.active

    啟用的 profiles

  • application.profiles.include

    要包含的 profiles,不會被 CLI 參數覆蓋

  • application.profiles.group

    分組

這些參數不能用在多個設定環境跟指定 profile 設定檔

ex: 以下是錯誤用法

在 application.yml

---
---
spring:
  config:
    activate:
      on-profile: main2
  profiles:
    active: main

在 application-dev.yml 使用 application.profiles.active

spring:
  profiles:
    active: main

設定加密

Spring Boot 沒有提供標準加解密的方法,但有提供 EnvironmentPostProcessor interface,可在 application 啟動前控制 spring 環境

spring 加解密的方法:

  • 使用第三方設定中心,支援自動解密~~~~

  • 使用自訂加解密

  • 使用 Jasypt Spring Boot

第三方設定

Spring Cloud Config

需要加解密的內容,前面會加上 {cipher}

自訂加解密

模仿 spring cloud 的方法

spring:
  datasource:
  username: '{cipher}XXXXXXXX'

用以下方式解密

@Bean
public DataSource dataSource() {
    DataSource dataSource = new DruidDataSource();
    String username = this.getUsername();
    if( username.startWith("{cipher}") ) {
        username = Encrypt.decrypt( username, this.getKey() );
    }
    dataSource.setUsername(username);
    //...
    return dataSource;
}

使用 Jasypt Spring Boot

ref: GitHub - ulisesbocchio/jasypt-spring-boot: Jasypt integration for Spring boot

ref: Spring boot 開發 - 使用 Jasypt 進行加密 | roi's blog

可針對 Spring Boot 專案中的屬性,提供加解密的方案

設定升級

當 Spring Boot 更新版本後,某些設定參數可能已經被修改或刪除,在 pom 加上這個 library 就能自動分析。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-properties-migrator</artifactId>
        </dependency>

在啟動後,就可在 console 看到需要調整的參數

  1. 在程式啟動後,再加入 Spring 的設定參數,這個自動分析不支援。ex: @PropertySource 載入的設定

  2. 設定修改完成後,要將此 library 移除

2025/02/24

Spring Boot 3 - Basic

Spring 核心有兩個技術:IoC (Invertion of Control) 控制反轉 與 AOP (Aspect Oriented Programming)

IoC 就是以 DI (Dependency Injection) 實現,不修改程式就能把某一個變數所參考到的物件換成某一個相容的物件。

AOP 就是以 proxy design pattern 區分商業與一般業務邏輯,讓開發者更聚焦商業邏輯的開發,一般業務邏輯例如使用者權限檢查、資料庫交易 transaction,或是 log 記錄,這些重複的通用功能。很像是將 business logic functions 以橫切面的方式,加上一般業務邏輯的一些通用功能。

Spring Boot 以 Convention over Configuration 的概念,減少大量設定,只需要定義預設以外的設定。

核心模組

  1. spring-boot

    這是 Spring Boot 最主要的模組,主要功能:

    • 啟動 Spring application 的主類別,有靜態 method 能產生 Spring container 的 context

    • 提供內嵌可自由搭配的 servlet container,例如 Tomcat, Jetty, Undertow

  2. spring-boot-autoconfigure

    常用的自動設定模組,可用 @EnableAutoConfiguration 來做自動設定

  3. spring-boot-starters

    這是所有 starter 的基礎

  4. spring-boot-cli

    CLI 工具,這也是產生 spring application 的方法

  5. spring-boot-actuator

    application 監控模組

  6. spring-boot-actuator-autoconfigure

    為 spring-boot-actuator 提供自動設定的模組

  7. spring-boot-test

    測試模組

  8. spring-boot-test-autoconfigure

    為 spring-boot-test 提供自動設定的模組

  9. spring-boot-loader

    可將 spring boot application 打包成一個可單獨執行的 jar,用 java -jar 執行

  10. spring-boot-devtools

    開發者工具,用在 application 的開發階段。ex: 修改程式可自動重新啟動 application。打包後會自動被禁用。

spring-boot-starter-web 依賴於 spring-webmvc,spring-webmvc 依賴於 spring-beans, spring-core。

Spring Cloud - Spring Boot - Spring MVC - Spring 這四個由上到下的關係

版本

  1. GA (General Availability) 正式版

  2. Current:最新的 GA 正式版

  3. SNAPSHOT:最新的變更,每天編譯的版本

  4. PRE:預覽版

    • Milestore:例如 3.0.0-M3

    • Release Candidate RC:例如 3.0.0-RC2

系統需求

ref: System Requirements :: Spring Boot

Spring Boot 3.3.5 的開發環境需求

  • JDK 17+

  • Spring 6.1.14+

  • Maven 3.6.3+

  • Gradle 7.5+, 8.x

Servlet Container 的版本,必須要是 servlet 5+ 相容的 container

  • Tomcat 10+

  • Jetty 11+

  • Undertow 2.2+

Spring Boot application 可透過 GraalVM 22.3+ 的 native-image tool 或是 Gradle/Maven 的 native build tools plugins 被轉換為 Native Image。

  • GraalVM Community 22.3

  • Native Build Tools 0.10.3

Demo Project

到網站 https://start.spring.io/ 可產生 Spring Boot 的專案

原本預設右邊 Dependencies 是空的,這邊因為要開發一個簡單的 web service,所以加上 Spring Web library。Package 方式有兩種,jar 跟 war,jar 是可以直接執行的,war 必須放到一個 Servlet container 裡面執行,這邊是最後封裝的結果,開發時還是用單獨的 application 執行。

產生的專案,用 Generate 可下載到 zip,解壓縮後,用 IDE 打開

pom.xml 裡面因為 web dependencies 的關係,多了 spring-boot-starter-web 與 spring-boot-starter-tomcat,因封裝方式為 war,所以 spring-boot-starter-tomcat 必須標記 provided

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tw.com.test</groupId>
    <artifactId>demo1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>demo1</name>
    <description>Test project for Spring Boot</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project

專案中有 mvnw 的 script,這是 maven wrapper。mvnw 只是 mvn 的封裝,指令參數一樣,如果有自己安裝了 maven,就不需要使用 mvnw。

修改 Demo1Application.java

package tw.com.test.demo1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class Demo1Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

    @RequestMapping("/hello")
    public String helloWorld() {
        return "hello world";
    }
}

加上 @RestController 以及 @RequestMapping("/hello") 這兩個部分。

專案中還有一個 ServletInitializer.java,這是因為 packaging 方式設定為 war 才有的。

啟動方式,可在 IDE 裡面執行 Demo1Application,或是在 console 用以下方式啟動

$ mvn spring-boot:run
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< tw.com.test:demo1 >--------------------------
[INFO] Building demo1 0.0.1-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] >>> spring-boot-maven-plugin:3.3.5:run (default-cli) > test-compile @ demo1 >>>
[INFO]
[INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ demo1 ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- maven-compiler-plugin:3.13.0:compile (default-compile) @ demo1 ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 2 source files with javac [debug parameters release 17] to target/classes
[INFO]
[INFO] --- maven-resources-plugin:3.3.1:testResources (default-testResources) @ demo1 ---
[INFO] skip non existing resourceDirectory /Users/charley/project/idea/book/test/demo1/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.13.0:testCompile (default-testCompile) @ demo1 ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 1 source file with javac [debug parameters release 17] to target/test-classes
[INFO]
[INFO] <<< spring-boot-maven-plugin:3.3.5:run (default-cli) < test-compile @ demo1 <<<
[INFO]
[INFO]
[INFO] --- spring-boot-maven-plugin:3.3.5:run (default-cli) @ demo1 ---
[INFO] Attaching agents: []

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.5)

啟動後,可在 browser 瀏覽 http://localhost:8080/hello ,會看到網頁內容是 "hello world"

Spring Boot CLI

Spring Boot Command Line Interface 可跟  start.spring.io 一樣,產生一個新的 spring boot project,或是用來加密。

該工具是用打包在一起的 Groovy 實作。直接從 Installing Spring Boot :: Spring Boot 下載壓縮檔後,解壓縮,需要設定 SPRING_HOME 環境變數,以及 PATH要增加 $SPRING_HOME/bin

Spring Boot CLI 提供在 bash/zsh 自動補上指令的功能。

ln -s ./shell-completion/bash/spring /etc/bash_completion.d/spring
ln -s ./shell-completion/zsh/_spring /usr/local/share/zsh/site-functions/_spring

RestController vs Controller

RestController 是處理 RESTful Web method,通常用在頁面裡面的資料JSON 或 XML。

Controller 是傳統的 html/jsp 網頁,在 Spring Boot 可搭配 thymeleaf template engine 使用。

在剛剛的 Demo Project 裡面,修改 XML 增加 thymeleaf

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

新增 Test.java

package tw.com.test.demo1.controller;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Test {

    @GetMapping("/test")
    public String test(HttpServletRequest request) {
        request.setAttribute("name", "UserName");
        // "test" 會映射到 Thymeleaf templates/test.html
        return "test";
    }
}

新增 resources/templates/test.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>Thymeleaf Template</title>
</head>

<body>
<span th:text="${name}" /> ,Spring Boot!
</body>

</html>

啟動測試後,http://localhost:8080/hello 會看到 hello world 網頁內容。

http://localhost:8080/test 會看到 UserName ,Spring Boot! 網頁內容

IDE Plugin

如果是使用 Eclipse,可安裝 Spring Tool Suite (STS) plugin,該 plugin 有跟網站 https://start.spring.io/ 一樣的功能,可產生 Spring Boot 的專案

如果是使用 IDEA,在 New Project 可選擇 Spring Initializer,也有類似的功能。

2025/02/17

Queue in Java

Queue 是 java.util package 裡面的 Collection framwork 中的其中一個介面,主要是定義 First-in-First-out FIFO queue 這種介面。除了基本的 Collection 操作方法以外,Queue 還提供了專屬的 insert/get/inspect method。

throw Exception return false/null
insert add(e) offer(e)
remove remove() poll()
examine element() peek()
    @Test
    public void queue_test1() {
        Queue<String> queue = new LinkedList<>();
        queue.add("one");
        queue.add("two");
        queue.add("three");
        assertEquals("[one, two, three]", queue.toString());

        queue.remove("two");
        assertEquals("[one, three]", queue.toString());

        String element = queue.element();
        assertEquals("one", element);
        assertEquals("[one, three]", queue.toString());

        // To empty the queue
        queue.clear();
        queue.offer("one");
        queue.offer("two");
        queue.offer("three");
        assertEquals("[one, two, three]", queue.toString());

        // poll 是取得 queue 的第一個 element
        String pollElement = queue.poll();
        assertEquals("one", pollElement);
        assertEquals("[two, three]", queue.toString());

        // peek 是取得 queue 的第一個 element,但只是偷看,不會從 queue 移除該 element
        String peakElement = queue.peek();
        assertEquals("two", peakElement);
        assertEquals("[two, three]", queue.toString());
    }

    @Test
    public void queue_test2() {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(2);
        queue.add("one");
        queue.add("two");
        // offer 在 insert 超過 Queue 容量時,會產生 exception
        IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
            queue.add("three");
        });

        // offer 在 insert 超過 Queue 容量時,不會產生 exception
        // 只是回傳一個 true/false flag 代表這個 insert 有沒有成功
        queue.clear();
        queue.offer("one");
        assertTrue( queue.offer("two") );
        assertFalse( queue.offer("three") );
        assertEquals("[one, two]", queue.toString());

        queue.clear();
        // remove, element 在 Queue 沒有任何資料時,會產生 exception
        NoSuchElementException exception2 = assertThrows(NoSuchElementException.class, () -> {
            queue.remove();
        });
        NoSuchElementException exception3 = assertThrows(NoSuchElementException.class, () -> {
            queue.element();
        });
        // poll, peek 會在 Queue 為空的時候,回傳 null
        assertNull(queue.poll());
        assertNull(queue.peek());
    }

sub interfaces

Queue 有三個主要的子介面: Blocking Queue, Transfer Queue, Deque

Blocking Queue

增加 methods,可強制 threads 等待 queue,例如在取得 queue 的元素時,可一直等待 queue 裡面有元素才回傳。或是可以等待 queue 清空後,再新增元素時。

Blocking Queue 的實作包含了 LinkedBlockingQueue, SynchronousQueue 及ArrayBlockingQueue

除了既有的 add(), offer() 以外,另外還有

  • put()

    insert 一個元素,等待 queue 有空間才能 put 進去

  • offer(E e, long timeout, TimeUnit unit)

    insert 一個元素,等待 queue 有空間才能 put 進去,等待的時間有 timeout 機制

remove 部分除了既有的 remove(), poll() 以外,還有

  • take()

    取得第一個元素,當 queue 為空的時候,會 blocking thread,等待 queue 有元素可以取得

  • poll(long timeout, TimeUnit int)

    取得第一個元素,當 queue 為空的時候,會 blocking thread,等待 queue 有元素可以取得,等待的時間有 timeout 機制

import java.util.Random;
import java.util.concurrent.BlockingQueue;

class Producer extends Thread {
    protected BlockingQueue<Integer> blockingQueue;
    private int limit;

    Producer(BlockingQueue<Integer> blockingQueue, int limit) {
        this.blockingQueue = blockingQueue;
        this.limit = limit;
    }

    public void run() {
        Random random = new Random();
        for(int i = 1; i <= limit; i++) {
            try {
                // random 放入 1/2 個 integer
                int randomProducer = random.nextInt(2);
//                System.out.println("randomProducer=" + randomProducer);
                for(int j = 0; j <= randomProducer; j++) {
                    System.out.println("Producer put " + (i+j));
                    blockingQueue.put((i+j)); // to produce data
                }
                i = i+randomProducer;
                // produce data with an interval of 0.5 sec
                Thread.sleep(500);
            } catch (InterruptedException exp) {
                System.out.println("An interruption occurred at Producer");
            }
        }
    }
}

Consumer.java

import java.util.concurrent.BlockingQueue;

class Consumer extends Thread {
    protected BlockingQueue<Integer> blockingQueue;
    Consumer(BlockingQueue<Integer> blockingQueue) { // constructor
        this.blockingQueue = blockingQueue;
    }
    public void run() { // overriding run method
        try {
            while (true) {
                Integer elem = blockingQueue.take(); // to consume data
                System.out.println("Consumer take " + elem);
            }
        }
        // to handle exception
        catch (InterruptedException exp) {
            System.out.println("An interruption occurred at Consumer");
        }
    }
}

CPTest.java

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class CPTest {
    public static void main(String[] args) throws InterruptedException {
        // create an object of BlockingQueue
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(5);

        // passing object of BlockingQueue as arguments
        Producer threadProd = new Producer(blockingQueue, 20);
        Consumer threadCon = new Consumer(blockingQueue);

        // to start the process
        threadProd.start();
        threadCon.start();

        // to exit the process after 5 sec
        Thread.sleep(2000);
        System.exit(0);
    }
}

執行結果

Producer put 1
Producer put 2
Consumer take 1
Consumer take 2
Producer put 3
Consumer take 3
Producer put 4
Consumer take 4
Producer put 5
Consumer take 5

Transfer Queue

extends BlockingQueue 介面,並套用 producer-consumer pattern,可控制 producer 到 consumer 資料流動的速度。

Transfer Queue 的實作包含了 LinkedTrasferQueue。

Producer.java

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TransferQueue;

class Producer extends Thread {
    protected TransferQueue<Integer> transferQueue;
    private int limit;

    Producer(TransferQueue<Integer> transferQueue, int limit) {
        this.transferQueue = transferQueue;
        this.limit = limit;
    }

    public void run() {
        for(int i = 1; i <= limit; i++) {
            try {
                System.out.println("Producer put " + i);
                boolean added = transferQueue.tryTransfer(i, 4000, TimeUnit.MILLISECONDS);
                if( !added ) {
                    i = i-1;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Consumer.java

import java.util.concurrent.TransferQueue;

class Consumer extends Thread {
    protected TransferQueue<Integer> transferQueue;
    Consumer(TransferQueue<Integer> transferQueue) { // constructor
        this.transferQueue = transferQueue;
    }
    public void run() {
        try {
            while (true) {
                Integer elem = transferQueue.take(); // to consume data
                System.out.println("Consumer take " + elem);
            }
        } catch (InterruptedException exp) {
            System.out.println("An interruption occurred at Consumer");
        }
    }
}

TransferQueueTest.java

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;

public class TransferQueueTest {
    public static void main(String[] args) throws InterruptedException {
        TransferQueue<Integer> transferQueue = new LinkedTransferQueue<>();

        // passing object of BlockingQueue as arguments
        Producer threadProd = new Producer(transferQueue, 5);
        Consumer threadCon = new Consumer(transferQueue);

        // to start the process
        threadProd.start();
        threadCon.start();

        // to exit the process after 5 sec
        Thread.sleep(2000);
        System.exit(0);
    }
}

Deque

Deque 是 Double-Ended Queue 的縮寫,也就是雙向的 Queue,頭尾都可以 insert/get 資料

Deque 的實作包含了 ArrayDeque。

Operation Method Method throwing Exception
Insertion from Head offerFirst(e) addFirst(e)
Removal from Head pollFirst() removeFirst()
Retrieval from Head peekFirst() getFirst()
Insertion from Tail offerLast(e) addLast(e)
Removal from Tail pollLast() removeLast()
Retrieval from Tail peekLast() getLast()

測試程式

    @Test
    public void deque_test() {
        // Deque as Stack
        Deque<String> stack = new ArrayDeque<>();
        stack.push("one");
        stack.push("two");
        assertEquals("two", stack.getFirst());
        assertEquals("two", stack.pop());
        stack.pop();
        NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> {
            stack.pop();
        });

        // Deque as Queue
        Deque<String> queue = new ArrayDeque<>();
        queue.offer("one");
        queue.offer("two");
        assertEquals("two", queue.getLast());
        assertEquals("one", queue.poll());
        queue.poll();
        assertNull(queue.poll());
    }

Priority Queue

新的元素要加入 PriorityQueue 時,會立刻以 natural order 或是已經定義的 Comparator 排序

    @Test
    public void priority_queue_test() {
        PriorityQueue<String> integerQueue = new PriorityQueue<>();

        integerQueue.add("one");
        integerQueue.add("two");
        integerQueue.add("three");

        String first = integerQueue.poll();
        String second = integerQueue.poll();
        String third = integerQueue.poll();

        assertEquals("one", first);
        assertEquals("three", second);
        assertEquals("two", third);
    }

Reference

Guide to the Java Queue Interface | Baeldung

Java Queue – Queue in Java | DigitalOcean