2024/09/09

InfiniBand IB

作為網路互連的技術方案,Ethernet 跟 InfiniBand (IB) 兩者的發展目標不同,導致現在應用的領域跟範圍都不同。

  • 使用場景

一般人比較常聽到 Ethernet,Ethernet 用在區域網路中,可將多個網路設備連接起來,以光纖連接 Internet,以無線網路連接手持設備。InfiniteBand 主要用在HPC 與 data center,這類對高頻寬低延遲的網路連接需求較高的應用場景

  • 頻寬

Ethernet 的應用發展,並不是以高速頻寬為主要發展的重點,他的重點在讓多種異質網路終端能夠互相連接起來,所以頻寬並不是發展的重點。通常 Ethernet 是在 1Gbps 到 100 Gbps,而InfiniBand 可到 100 Gbps 或 200 Gbps,像這份報導的說明,InfiniBand進入400Gb/s世代,Nvidia新款網路交換器揭開序幕 | iThome 已經到了 400Gbps 的時代。

  • 應用範圍

Ethneret 面向一般終端的消費者,用來做個人使用的資料傳輸並連接 Internet。InfiniBand 是用在大量的資料運算,在現在最熱門的 AI 時代,要建構一個 AI cluster,會使用 InfiniBand 作為 cluster 節點的連接技術。

InfiniBand 另一個應用是在超級電腦裡面,因為超級電腦叢集也需要這種高速低延遲的資料傳輸。

  • 成本

InfiniBnad 的效能高,相對成本就很高,沒有特殊的需求,一般是不會使用 InfiniBand

  • 網路模式

InfiniBand 比 Ethernet 容易管理,每一個 end node 會透過一個 layer 2 switch 配置網路節點 ID,再往上以 Router 統計計算網路資料轉發路徑。

Ethernet 是用硬體的 MAC 網路模式,往上使用 IP 搭配 ARP 建構網路,且網路本身沒有學習機制,容易產生環狀網路,這又要透過 STP 協定解決,增加了網路的複雜度

Socket Direct Protocol (SDP)

Trail: Sockets Direct Protocol (The Java™ Tutorials)

Java 7 Sockets Direct Protocol – Write Once, Run Everywhere …. and Run (Some Places) Blazingly - InfoQ

SDP

在 JDK 7 裡面,就提供了一種不同於 TCP/IP 傳統的 Socket 網路的 Socket Direct Protocol,SDP 能夠透過 InifiBand 的 Remote Direct Memory Access (RDMA) ,以低延遲的方法,不透過 OS,遠端存取其他電腦的記憶體。

透過這張圖的說明,可以了解為什麼 SDP 能夠提供高速的通訊,傳統的 TCP/IP 需要一層一層接過 Application Layer -> Transport Layer -> Network Layer -> Physical Layer,SDP 則是從 Application 一步直接穿到 RDMA enabled channel adapter card

參考這個連結的圖片: SDP

References

InfiniBand - 維基百科,自由的百科全書

InfiniBand與以太網:它們是什麼? | 飛速(FS)社區

InfiniBand之技術架構介紹

2024/09/02

Quartz

Quartz 是 java 的 job-scheduling framwork,可使用類似 linux 的 crontab 方式設定定時啟動某個工作。

pom.xml

        <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>

API

quartz API 的核心是 scheduler,啟動後,Scheduler 會產生多個 worker threads,也就是利用 ThreadPool,執行 Jobs。

主要的 API interface

  • Scheduler

  • Job

  • JobDetail

    • 定義 job instances
  • Trigger

    • 決定 scheduler 的定時機制

    • SimpleTrigger 跟 CronTrigger 兩種

  • JobBuilder

    • 用來產生 JobDetail instances
  • TriggerBuilder

    • 用來產生 Trigger instances

Example

SimpleJob.java

package quartz;

import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

// 要實作 Job interface
public class SimpleJob implements Job {
    // trigger 會透過 scheduler 的 worker 呼叫 execute 執行 job
    // JobExecutionContext 可提供 runtime environment 資訊
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 在 產生 JobDetail 時,JobData 會透過 JobDataMap 傳進 Job
        // Job 利用 JobDataMap 取得 JobData
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        String jobSays = dataMap.getString("jobSays");
        float myFloatValue = dataMap.getFloat("myFloatValue");

        System.out.println("Job says: " + jobSays + ", and val is: " + myFloatValue);
    }
}

JobA.java

@DisallowConcurrentExecution 可限制 Job 不會同時被執行兩次

package quartz;

import org.quartz.*;

@DisallowConcurrentExecution
public class JobA implements Job {

    public void execute(JobExecutionContext context) throws JobExecutionException {
        TriggerKey triggerKey= context.getTrigger().getKey();
        System.out.println("job A. triggerKey="+triggerKey.getName()+", group="+triggerKey.getGroup() + ", fireTime="+context.getFireTime());
    }

}

JobB.java

package quartz;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.TriggerKey;

public class JobB implements Job {

    public void execute(JobExecutionContext context) throws JobExecutionException {
        TriggerKey triggerKey= context.getTrigger().getKey();
        System.out.println("job B. triggerKey="+triggerKey.getName()+", group="+triggerKey.getGroup() + ", fireTime="+context.getFireTime());
    }

}

QuartzExample.java

package quartz;

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzExample {

    public static void main(String args[]) {
        try {
            // 透過 SchedulerFactory 產生 Scheduler
            SchedulerFactory schedFact = new StdSchedulerFactory();
            Scheduler sched = schedFact.getScheduler();
            // 使用 start 啟動,使用 shutdown 停掉這個 scheduler
            // 要先將 job, trigger 綁定 scheduler 後,再啟動 scheduler
            // sched.start();
            // sched.shutdown();

            // 產生 SimpleJob,利用 JobData 傳入資料到 Job
            JobDetail job = JobBuilder.newJob(SimpleJob.class)
                                      .withIdentity("myJob", "group1")
                                      .usingJobData("jobSays", "Hello World!")
                                      .usingJobData("myFloatValue", 3.141f)
                                      .build();

            Trigger trigger = TriggerBuilder.newTrigger()
                                            .withIdentity("myTrigger", "group1")
                                            .startNow()
                                            .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(40).repeatForever())
                                            .build();

            ///////
            JobDetail jobA = JobBuilder.newJob(JobA.class)
                                       .withIdentity("jobA", "group2")
                                       .build();
            JobDetail jobB = JobBuilder.newJob(JobB.class)
                                       .withIdentity("jobB", "group2")
                                       .build();

            // triggerA 比 triggerB 有較高的 priority,比較早先被執行
            // 每 40s 啟動一次
            Trigger triggerA = TriggerBuilder.newTrigger()
                                             .withIdentity("triggerA", "group2")
                                             .startNow()
                                             .withPriority(15)
                                             .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(40).repeatForever())
                                             .build();

            // 每 20s 啟動一次
            Trigger triggerB = TriggerBuilder.newTrigger()
                                             .withIdentity("triggerB", "group2")
                                             .startNow()
                                             .withPriority(10)
                                             .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(20).repeatForever())
                                             .build();

            sched.scheduleJob(job, trigger);
            sched.scheduleJob(jobA, triggerA);
            sched.scheduleJob(jobB, triggerB);
            sched.start();

        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

}

執行結果

Job says: Hello World!, and val is: 3.141
job A. triggerKey=triggerA, group=group2, fireTime=Wed Aug 28 16:16:37 CST 2024
job B. triggerKey=triggerB, group=group2, fireTime=Wed Aug 28 16:16:37 CST 2024
job B. triggerKey=triggerB, group=group2, fireTime=Wed Aug 28 16:16:57 CST 2024
Job says: Hello World!, and val is: 3.141
job A. triggerKey=triggerA, group=group2, fireTime=Wed Aug 28 16:17:17 CST 2024
job B. triggerKey=triggerB, group=group2, fireTime=Wed Aug 28 16:17:17 CST 2024

QuartzExample2.java

package quartz;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzExample2 {

    public static void main(String args[]) {
        try {
            SchedulerFactory schedFact = new StdSchedulerFactory();
            Scheduler sched = schedFact.getScheduler();

            // 產生 SimpleJob,利用 JobData 傳入資料到 Job
            JobDetail job = JobBuilder.newJob(SimpleJob.class)
                                      .withIdentity("myJob", "group1")
                                      .usingJobData("jobSays", "Hello World!")
                                      .usingJobData("myFloatValue", 3.141f)
                                      .build();

            Trigger trigger = TriggerBuilder.newTrigger()
                                            .withIdentity("myTrigger", "group1")
                                            .startNow()
                                            .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(40).repeatForever())
                                            .build();

            // 跟上面的 Scheduler 一樣
            // 如果 worker thread 不足,job 可能不會被執行
            // 當 quartz 發現有這種狀況時,會使用 .withMisfireHandlingInstructionFireNow() 這個規則
            // 在 misfire 時,馬上執行一次
            Trigger trigger2 = TriggerBuilder.newTrigger()
                                            .withIdentity("myTrigger", "group1")
                                            .startNow()
                                            .withSchedule(SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow().withIntervalInSeconds(40).repeatForever())
                                            .build();

            sched.scheduleJob(job, trigger);
            sched.start();

        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

}

QuartzExample3.java

package quartz;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Date;

public class QuartzExample3 {

    public static void main(String args[]) {
        try {
            SchedulerFactory schedFact = new StdSchedulerFactory();
            Scheduler sched = schedFact.getScheduler();

            // 產生 SimpleJob,利用 JobData 傳入資料到 Job
            JobDetail jobA = JobBuilder.newJob(JobA.class)
                                       .withIdentity("jobA", "group2")
                                       .build();

            // SimpleTrigger 可設定在某個特定時間開始
            // 3 seconds 後啟動 trigger
            // 同樣可加上 SimpleScheduleBuilder 定時每 40s 啟動一次
            java.util.Calendar calendar = java.util.Calendar.getInstance();
            calendar.add(java.util.Calendar.SECOND, 3);
            Date myStartTime = calendar.getTime();
            SimpleTrigger trigger =
                    (SimpleTrigger) TriggerBuilder.newTrigger()
                                                  .withIdentity("trigger1", "group1")
                                                  .startAt(myStartTime)
                                                  .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(40).repeatForever())
                                                  .build();

            JobDetail jobB = JobBuilder.newJob(JobB.class)
                                       .withIdentity("jobB", "group1")
                                       .build();
            // 透過 CronScheduleBuilder 產生 scheduler
            // "0 0/1 * * * ?" 代表每分鐘執行一次
            // 然後產生 CronTrigger
            CronTrigger trigger2 = TriggerBuilder.newTrigger()
                                                 .withIdentity("trigger2", "group1")
                                                 .withSchedule(CronScheduleBuilder.cronSchedule("0 0/1 * * * ?"))
                                                 .build();

            sched.scheduleJob(jobA, trigger);
            sched.scheduleJob(jobB, trigger2);
            sched.start();

        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

}

執行結果

job B. triggerKey=trigger2, group=group1, fireTime=Wed Aug 28 16:11:00 CST 2024
job A. triggerKey=trigger1, group=group1, fireTime=Wed Aug 28 16:11:00 CST 2024
job A. triggerKey=trigger1, group=group1, fireTime=Wed Aug 28 16:11:40 CST 2024
job B. triggerKey=trigger2, group=group1, fireTime=Wed Aug 28 16:12:00 CST 2024
job A. triggerKey=trigger1, group=group1, fireTime=Wed Aug 28 16:12:20 CST 2024

References

Introduction to Quartz | Baeldung

2024/08/26

SpringMVC + Hibernate + Spring WebSocket 的 web sample project

可依照以下順序查看 source code

configurations

pom.xml

在POM檔加入Spring MVC的dependency

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>camiol</groupId>
    <artifactId>smvc</artifactId>
    <packaging>war</packaging>
    <version>0.0.1-SNAPSHOT</version>

    <name>springMVC-Demo Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>6.1.4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>6.1.4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
            <version>6.1.4</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-orm -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>6.1.4</version>
        </dependency>

        <!-- hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.4.4.Final</version>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-c3p0</artifactId>
            <version>6.4.4.Final</version>
        </dependency>

        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- JSTL -->
        <dependency>
            <groupId>jakarta.servlet.jsp.jstl</groupId>
            <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
            <version>3.0.0</version>
        </dependency>

        <dependency>
            <groupId>org.glassfish.web</groupId>
            <artifactId>jakarta.servlet.jsp.jstl</artifactId>
            <version>3.0.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version>
        </dependency>

    </dependencies>


    <build>
        <finalName>smvc</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <warSourceDirectory>src/main/webapp</warSourceDirectory>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

web.xml

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1">

    <display-name>SpringMVC-Demo Project</display-name>

    <!-- Spring MVC DispatcherServlet -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:config/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

spring-mvc.xml

src/main/resources/config/spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/mvc 
                           http://www.springframework.org/schema/mvc/spring-mvc.xsd
                           http://www.springframework.org/schema/context 
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd
                           http://www.springframework.org/schema/websocket
                           http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd">

    <mvc:annotation-driven />
    <mvc:resources location="/js/" mapping="/js/**"/>
    <context:annotation-config />

    <!-- Database Configuration -->
    <import resource="../database/datasource.xml" />
    <import resource="../database/hibernate.xml" />

    <context:component-scan
        base-package="spring.demo" />

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/" />
        <property name="suffix" value=".jsp" />
    </bean>

</beans>

database.properties

src/main/resources/properties/database.properties

# DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/smvc?useUnicode=yes&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=pwd
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# C3P0
connectionPool.init_size=2
connectionPool.min_size=2
connectionPool.max_size=10
connectionPool.timeout=600

# SQL
jdbc.dataSource.dialect=org.hibernate.dialect.MySQLDialect
jdbc.dataSource.showSql=true

datasource.xml

src/main/resources/database/datasource.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/mvc 
                           http://www.springframework.org/schema/mvc/spring-mvc.xsd
                           http://www.springframework.org/schema/context 
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean
        class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
        <property name="location">
            <value>classpath:properties/database.properties</value>
        </property>
    </bean>

    <bean id="dataSource"
        class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass"
            value="${spring.datasource.driver-class-name}" />
        <property name="jdbcUrl" value="${spring.datasource.url}" />
        <property name="user" value="${spring.datasource.username}" />
        <property name="password" value="${spring.datasource.password}" />
        <property name="autoCommitOnClose" value="false"/> <!-- 連接關閉時默認將所有未提交的操作回復 Default:false -->
        <property name="checkoutTimeout" value="${connectionPool.timeout}"/>
        <property name="initialPoolSize" value="${connectionPool.init_size}"/>
        <property name="minPoolSize" value="${connectionPool.min_size}"/>
        <property name="maxPoolSize" value="${connectionPool.max_size}"/>
    </bean>

</beans>

hibernate.xml

src/main/resources/database/hibernate.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/mvc 
                           http://www.springframework.org/schema/mvc/spring-mvc.xsd
                           http://www.springframework.org/schema/context 
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- Hibernate session factory -->
    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">

        <property name="dataSource">
            <ref bean="dataSource" />
        </property>
        <property name="packagesToScan">
            <list>
                <value>spring.demo.entity</value>
            </list>
        </property>

        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">${jdbc.dataSource.dialect}</prop>
                <prop key="hibernate.show_sql">${jdbc.dataSource.showSql}</prop>
            </props>
        </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

mysql database

create table Student
(
   id         int(12) not null auto_increment,
   Name       national varchar(50),
   Math_Score int,
   primary key (id)
);

insert into Student (Name, Math_Score) values ('Aron', 33);
insert into Student (Name, Math_Score) values ('Benson', 22);

java source code

HelloController

src/main/java/spring/demo/controller/HelloController.java

package spring.demo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import spring.demo.entity.Student;
import spring.demo.service.StudentService;

@Controller
public class HelloController {
    @Autowired
    private StudentService service;

    @RequestMapping("hello")
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
    @RequestMapping("list")
    public ModelAndView list() {
        List<Student> resultList = service.findStudent();
        ModelAndView model = new ModelAndView();
        model.setViewName("list");
        model.addObject("resultList",resultList);

        return model;
    }

    @RequestMapping(value = "view" ,method = RequestMethod.POST)
    public ModelAndView view(@RequestParam("id") long id,@RequestParam("type") String type) {
        System.out.println(id);
        System.out.println(type);
        Student s = service.findStudent(id);
        ModelAndView model = new ModelAndView();
        model.setViewName("view");
        model.addObject("s", s);
        model.addObject("type",type);
        return model;
    }

    @RequestMapping(value = "update" ,method = RequestMethod.POST)
    public ModelAndView update(@ModelAttribute("s") Student s,RedirectAttributes attr) {
        service.updateStudent(s);

        attr.addFlashAttribute("message","修改成功");
        return new ModelAndView("redirect:hello");
    }
}

Student

src/main/java/spring/demo/entity/Student.java

package spring.demo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

@Entity
@Table(name = "Student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(name="Name")
    private String name;
    @Column(name="Math_Score")
    private int mathScore;

    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getMathScore() {
        return mathScore;
    }
    public void setMathScore(int mathScore) {
        this.mathScore = mathScore;
    }

    public String toString() {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        return gson.toJson(this);
    }

}

Student Service

src/main/java/spring/demo/service/StudentSerivce.java

package spring.demo.service;

import java.util.List;

import spring.demo.entity.Student;

public interface StudentService {
    List<Student> findStudent();
    Student findStudent(long id);
    void updateStudent(Student s);
}

src/main/java/spring/demo/service/impl/StudentSerivceImpl.java

package spring.demo.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import spring.demo.dao.StudentDao;
import spring.demo.entity.Student;
import spring.demo.service.StudentService;

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentDao dao;

    @Override
    @Transactional
    public List<Student> findStudent() {
        return dao.findAll();
    }

    @Override
    @Transactional
    public Student findStudent(long id) {
        return dao.findById(id);
    }

    @Override
    @Transactional
    public void updateStudent(Student s) {
        dao.update(s);
    }

}

Student DAO

src/main/java/spring/demo/dao/StudentDao.java

package spring.demo.dao;

import java.util.List;

import spring.demo.entity.Student;

public interface StudentDao {
    Student findById(long id);
    List<Student> findAll();
    void update(Student s);
}

src/main/java/spring/demo/dao/impl/StudentDaoImpl.java

package spring.demo.dao.impl;

import java.util.List;

import jakarta.persistence.Query;

import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import spring.demo.dao.StudentDao;
import spring.demo.entity.Student;

@Repository
public class StudentDaoImpl implements StudentDao {
//    有多個sessionFactory 可以用Resource來指定
//    @Resource(name = "sessionFactory") 
//    private SessionFactory sessionFactory;

    @Autowired
    private SessionFactory sessionFactory;

    @Override
    @Transactional
    public Student findById(long id) {
        String sql = "select * from Student where id = :id";
        Query query = sessionFactory.getCurrentSession().createNativeQuery(sql).addEntity(Student.class);
        query.setParameter("id", id);
        return (Student) query.getSingleResult();
    }

    @Override
    @Transactional
    public List<Student> findAll() {
        String sql = "select * from Student";
        Query query = sessionFactory.getCurrentSession().createNativeQuery(sql).addEntity(Student.class);

        @SuppressWarnings("unchecked")
        List<Student> resultList = query.getResultList();
        return resultList;
    }

    @Override
    @Transactional
    public void update(Student s) {
        sessionFactory.getCurrentSession().update(s);
    }

}

WebSocket

src/main/java/spring/demo/ws/WebSocketConfig.java

package spring.demo.ws;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/chat").setAllowedOrigins("*").addInterceptors(new HttpSessionHandshakeInterceptor());
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new ChatHandler();
    }

}

src/main/java/spring/demo/ws/ChatHandshakeInterceptor.java

package spring.demo.ws;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

import java.util.Map;

@Component
public class ChatHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println("Before Handshake");
//        //在握手之前将HttpSession中的用户,copy放到WebSocket Session中
//        if (request instanceof ServletServerHttpRequest){
//            ServletServerHttpRequest servletServerHttpRequest=
//                    (ServletServerHttpRequest) request;
//            HttpSession session=
//                    servletServerHttpRequest.getServletRequest().getSession(true);
//            if (null!=session){
//                User user=(User)session.getAttribute("user");
//                //WebSocket Session
//                attributes.put("user",user);
//            }
//        }
        return super.beforeHandshake(request,response,wsHandler,attributes);
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
        super.afterHandshake(request, response, wsHandler, ex);
    }
}

src/main/java/spring/demo/ws/ChatHandler.java

package spring.demo.ws;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
public class ChatHandler extends TextWebSocketHandler {
    private static CopyOnWriteArraySet<WebSocketSession> clients=new CopyOnWriteArraySet<WebSocketSession>();
    private WebSocketSession wsSession = null;
    private String username;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        this.wsSession = session;
        String name = this.getAttribute(session, "username");
        this.username = name;
        clients.add(session);

        String message = (username+" is in chatroom");
        broadcastMessage(message);
    }

    private String getAttribute(WebSocketSession webSocketSession, String key) {
        URI uri = webSocketSession.getUri();
        //userid=123&dept=4403
        String query = uri.getQuery();
        if (null != query && !"".equals(query)) {
            //??
            String[] queryArr = query.split("&");
            for (String queryItem : queryArr) {
                //userid=123
                String[] queryItemArr = queryItem.split("=");
                if (2 == queryItemArr.length) {
                    if (key.equals(queryItemArr[0]))
                        return queryItemArr[1];
                }
            }
        }
        return "";
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
//        clients.remove(session.getId());
        clients.remove(session);
    }

    public static CopyOnWriteArraySet<WebSocketSession> getClients() {
        return clients;
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        broadcastMessage(username + " broadcast: " + payload);

        TextMessage s1 = new TextMessage(username+ " echo: " + payload);
        session.sendMessage(s1);
    }

    void broadcastMessage(String json) throws IOException {
        TextMessage message = new TextMessage(json);
        for( WebSocketSession client: getClients()) {
            client.sendMessage(message);
        }
    }
}

web page

src/main/webapp/WEB-INF/pages/hello.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="<c:url value='/js/jquery-3.7.1.min.js' />"></script>
<title>HelloWorld</title>
</head>
<body>
    <h1>Hello World!! Spring MVC</h1>
    <input type="button" id="getAll" value="列出全部學生" />
</body>
<script type="text/javascript">
    $(document).ready(function() {
        if ('${message}' != '') {
            alert('${message}')
        }

        $("#getAll").click(function() {
            window.location.href = "<c:url value='/list' />"
        });
    })
</script>
</html>

src/main/webapp/WEB-INF/pages/list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="<c:url value='/js/jquery-3.7.1.min.js' />"></script>
<title>List</title>
</head>
<body>
    <h1>學生列表</h1>
    <form id="viewDetail" action="view" method="post">
        <input name="id" type="hidden" /> <input name="type" type="hidden" />
        <table>
            <c:if test="${resultList.size()>0}">
                <tr>
                    <td>學號</td>
                    <td>名字</td>
                    <td>操作</td>
                </tr>
                <c:forEach items="${resultList}" var="result" varStatus="s">
                    <tr>
                        <td>${result.id}</td>
                        <td>${result.name}</td>
                        <td><input type="button" value="查看"
                            onclick="view(${result.id},'view')"></td>
                    </tr>
                </c:forEach>
            </c:if>
        </table>
    </form>
</body>
<script type="text/javascript">
    $(document).ready(function() {

    })
    function view(id,type){
        $('input[name="id"]').val(id);
        $('input[name="type"]').val(type);
        $("#viewDetail").submit();
    }
</script>
</html>

src/main/webapp/WEB-INF/pages/view.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="<c:url value='/js/jquery-3.7.1.min.js' />"></script>
<title>View Detail</title>
</head>
<body>
    <h1>詳細資料</h1>
    <form:form id="viewDetail" action="view" method="post" modelAttribute="s">
        <form:input path="id" type="hidden" /> <input name="type" type="hidden" />
        <table>
            <tr>
                <td>學號</td>
                <td>名字</td>
                <td>數學成績</td>
            </tr>

            <tr>
                <td>${s.id}</td>
                <c:choose>
                    <c:when test="${type eq 'view'}">

                        <td>${s.name}</td>
                        <td align="center">${s.mathScore}</td>
                    </c:when>
                    <c:otherwise>
                            <td><form:input path="name" /> </td>
                            <td align="center"><form:input path="mathScore" /></td>
                    </c:otherwise>
                </c:choose>
            </tr>

        </table>
    </form:form>
    <input type="button" value="回上ㄧ頁" onclick="history.back()" />
    <c:choose>
        <c:when test="${type eq 'view'}">
            <input type="button" value="修改"
                onclick="modifyDetail(${s.id}),'modify'" />
        </c:when>
        <c:otherwise>
            <input type="button" value="確認修改" onclick="update()" />
        </c:otherwise>
    </c:choose>
</body>
<script type="text/javascript">
    $(document).ready(function() {

    })
    function modifyDetail(id,type){
        $('input[name="id"]').val(id);
        $('input[name="type"]').val(type);
        $("#viewDetail").submit();
    }
    function update(){
        $("#viewDetail").attr("action","update").submit();
    }
</script>
</html>

啟動測試

Student 網頁 http://localhost:8080/smvc/hello

WebSocket 網頁 http://localhost:8080/smvc/ws.jsp

References

建立一個SpringMVC + Spring + Hibernate 的Web專案 - HackMD

Spring WebSocket - using WebSocket in a Spring application

spring框架下基于websocket握手的拦截器配置(HandshakeInterceptor)-CSDN博客