Hôm bữa hỗ trợ một bạn mình mới thấy rằng đang có sự hiểu nhầm nào đó dẫn đến các bạn gặp khó khăn trong việc triển khai dự án spring boot với tomcat. Vậy nên trong bài này mình sẽ mô tả rõ hơn về kiến trúc phần mềm khi sử dụng spring boot với tomcat để các bạn hình dung và tìm ra cách triển khai phù hợp nhé.

Cách chạy một phần mềm (chương trình) java

Có hai cách chạy một phần mềm java:

  1. Chạy lệnh: java -jar application.jar nếu toàn bộ phần mềm được đóng gói vào một tập tin jar duy nhất.
  2. Chạy lệnh: java -cp $CLASSPATH <tên lớp main>, ví dụ:
CLASSPATH="lib/*:settings"
echo 'classpath: ' $CLASSPATH
java $1 -cp $CLASSPATH com.tvd12.freechat.ApplicationStartup

Ở đây có một khái niệm là classpath, bạn có thể hiện đơn giản là một biến chứa các thư mục mà bạn muốn jvm (máy ảo java) quản lý và tải khi cần thiết. Bạn có thể tìm hiểu thêm về classpath tại đây.

Tomcat web container

Nghe thì hơi khó hiểu, nhưng thực tế nó là một trong những ứng dụng của việc sử dụng CLASSPATH, cụ thể chúng ta có thể nhìn vào cấu trúc thư mục của Tomcat.

!

Ở đây có rất nhiều thư mục ví dụ thư mục bin chứa các tập tin script để khởi chạy tomcat, ví dụ tập tin catalina.sh có chứa đoạn script sau:

  eval exec "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \
      -D$ENDORSED_PROP="\"$JAVA_ENDORSED_DIRS\"" \
      -classpath "\"$CLASSPATH\"" \
      -Djava.security.manager \
      -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \
      -Dcatalina.base="\"$CATALINA_BASE\"" \
      -Dcatalina.home="\"$CATALINA_HOME\"" \
      -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
      org.apache.catalina.startup.Bootstrap "$@" start

Trong đó CLASSPATH được ghép lại bởi các điều kiện:

# Add on extra jar files to CLASSPATH
if [ ! -z "$CLASSPATH" ] ; then
  CLASSPATH="$CLASSPATH":
fi
CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar

if [ -z "$CATALINA_OUT" ] ; then
  CATALINA_OUT="$CATALINA_BASE"/logs/catalina.out
fi

if [ -z "$CATALINA_TMPDIR" ] ; then
  # Define the java.io.tmpdir to use for Catalina
  CATALINA_TMPDIR="$CATALINA_BASE"/temp
fi

# Add tomcat-juli.jar to classpath
# tomcat-juli.jar can be over-ridden per instance
if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then
  CLASSPATH=$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar
else
  CLASSPATH=$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar
fi

Tiếp theo chúng ta có thể dành sự chú ý nhiều hơn cho thư mục webapps, ở đây có rất nhiều thư mục, mỗi thư mục đại diện cho một “ứng dụng” về bản chất là một dự án mà chúng ta có thể viết bằng spring hay bất kỳ framework nào tuân theo chuẩn của java web để tomcat có thể hiểu và tải được. Các thư mục này sẽ được đưa vào classpath và sau đó được quản lý bởi jvm.

Khi bạn tạo ra một dự án spring bạn sẽ đóng gói theo chuẩn war, bạn sẽ triển khai tập tin war này vào thư mục webapps, ví dụ manager.war, bạn có thể tự bung thư mục này ra hoặc nếu tomcat đang chạy nó có thể bung ra giúp bạn.

Khi bạn khởi động lại tomcat thì dự án sẽ được coi là một ứng dụng, ví dụ ứng dụng manager có thể được truy cập thông qua địa chỉ http://localhost:8080/manager/xxx Đây là một trong những cái cực kỳ khó chịu ngày trước mình hay gặp phải, tức là mình muốn truy cập đến các trang web ở địa chỉ http://localhost:8080/xxx thay vì http://localhost:8080/manager/xxx, tuy nhiên tomcat web container được thiết kế theo một kiến trúc cho phép nhiều ứng dụng chạy trên một máy chủ để có thể linh hoạt trong việc bổ sung hoặc bỏ đi một ứng dụng. Tuy nhiên thông thường thì mình chỉ triển khai một ứng dụng duy nhất thôi nên cái hay của tomcat lại thành cái dở của mình, lẽ ra là phải cấu hình các tập tin trong thư mục conf (mình cũng chưa cấu hình bao giờ) nhưng toàn hack là xoá thư mục webapps/ROOT đi rồi giải nén dự án của mình từ file war ra rồi đổi tên thành ROOT.

Tư tưởng triển khai này cũng khá hay, khi có thể chia một ứng dụng to thành các ứng dụng con, tuy nhiên đối với các hệ thống microservice thì lại thường hay để mỗi ứng dụng là một server riêng, với cả theo quan điểm cá nhân của mình thì tomcat chưa thực sự làm triển để cái phần triển khai đến mức cực kỳ tiện cho người sử dụng là các dev hoặc đội ngũ triển khai.

Tomcat Embedded

Sau khi thấy được những nhược điểm của chính mình và nhu cầu của người sử dụng, tomcat đã tạo ra một phiên bản mới gọi là tomcat embeded, bạn có thể tham khảo thêm tại đây. Bạn có thể tạo ra hàm main của dự án thay vì phải cấu hình rất phức tập tập tin web.xml và thừa kế đủ các lớp. Mã nguồn khởi tạo tomcat với hàm main sẽ đơn giản thế này thôi:

public class WebMvcStartUp {

    public static void main(String[] args) throws Exception {
        String webappDirLocation = "src/main/resources/";
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector();
        connector.setPort(8080);
        tomcat.setConnector(connector);
        tomcat.addWebapp(
            "/",
            new File(webappDirLocation).getAbsolutePath()
        );
        tomcat.start();
        System.out.println("server started");
        tomcat.getServer().await();
    }
}

Spring boot tomcat

Mặc định thì spring-boot-starter-web sử dụng tomcat embdedd khi bạn khai báo dependency như thế này:

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

Như vậy việc mà bạn cố gắng phải sử dụng kết hợp spring boot và tomcat web container là không nên, nó vừa không tận dụng được sức mạnh của cả spring boot lẫn tomcate embdded mà lại vừa khó cấu hình triển khai. Tuy nhiên mình nghĩ rằng việc sử dụng kết hợp này thường là do nhiều bạn hiểu nhầm rằng phải có tomcat web container thì mới triển khai được ứng dụng web viết bằng spring boot.
Khi cần triển khai, bạn có thể đóng gói toàn bộ dự án thành một tập tin jar duy nhất với plugin spring-boot-maven-plugin, ví dụ:

<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <configuration>
                    <mainClass>vn.techmaster.thymeleaf.ThymeleafStartUp</mainClass>
                    <outputDirectory>deploy</outputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Mã nguồn đầy đủ bạn có thể tham khảo tại đây.

Tổng kết

Như vậy chúng ta đã cùng nhau tìm hiểu về Tomcat web container và Tomcat Embedded, mình hy vọng các bạn sẽ lựa chọn cho các dự án mới của mình cách triển khai với Tomcat Embedded cho đơn giản nhé.


Cám ơn bạn đã quan tâm đến bài viết|video này. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a