2022/05/30

RSocket + WebSocket

Reactive Programming

這是一種以資料流為主的程式開發模型,目前比較流行的 Vue, React 也是類似這個概念,畫面上顯示的運算結果或呈現的資料,會隨時跟著資料異動而自動更新。

傳統的程式設計是用 Interative Programming 方法,例如

b:=2;
c:=3;
a:=b+c
// a 為 5
b:=3;
// 當 b 的值異動後,必須重新運算一次 b+c,才能更新 a

Excel 就是使用 Reactive Programming,當在某個儲存格 C1 填寫為 C1=A1+B1,C1 的值,就會隨著 A1 與 B1 異動而自動更新。

RSocket

rsocket 是類似 gRPC 的訊息傳遞協定,支援 TCP, WebSocket, Aeron(UDP),主要有四種互動的方式

Interaction Model Behavior
fire-and-forget 不需要 response
request-and-response one-to-one 傳統通訊,一個 request,一個response,然後不斷重複
request-response-stream one-to-many 發一個訊息,可連續收到多個 response
channel many-to-many bi-directional stream

Sample

ref: RSocket + WebSocket + Spring Boot = Real Time Application

這個網頁用 spring boot 做了stream 及 channel 兩個 sample server

client 部分是用 rsocket-js,透過 nodejs 啟動

Server

用 Java Sprint Boot 撰寫

pom.xml

<?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>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.vinsguru</groupId>
    <artifactId>rsocket-websocket</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rsocket-websocket</name>
    <description>Demo project for Spring Boot, RSocket WebSocket</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-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>

resources/application.properties

spring.rsocket.server.port=6565
spring.rsocket.server.mapping-path=/rsocket
spring.rsocket.server.transport=websocket

RsocketWebsocketApplication

package com.vinsguru.rsocketwebsocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RsocketWebsocketApplication {

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

}

RSocketController

package com.vinsguru.rsocketwebsocket.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;

import java.time.Duration;

@Controller
public class RSocketController {

    @MessageMapping("number.stream")
    public Flux<Integer> responseStream(Integer number) {
        return Flux.range(1, number)
                .delayElements(Duration.ofSeconds(1));
    }

    @MessageMapping("number.channel")
    public Flux<Long> biDirectionalStream(Flux<Long> numberFlux) {
        return numberFlux
                .map(n -> n * n)
                .onErrorReturn(-1L);
    }

}
  1. number.stream 就是 request-response-stream,client 會發送一個 Integer 給 Server,Server 會定時回傳 1, 2 ..... 直到該 Integer 停止
  2. number.channel 是 many-to-many 的傳輸,可連續發送多個 Long 給 Server,Server 會回傳該數的平方

Client

最主要的是 index.js

有兩個部分,分別是 number.stream 與 number.channel

import { RSocketClient, JsonSerializer, IdentitySerializer } from 'rsocket-core';
import RSocketWebSocketClient from 'rsocket-websocket-client';
import {FlowableProcessor} from 'rsocket-flowable';

// backend ws endpoint
const wsURL = 'ws://localhost:6565/rsocket';

// rsocket client
const client = new RSocketClient({
    serializers: {
        data: JsonSerializer,
        metadata: IdentitySerializer
    },
    setup: {
        keepAlive: 60000,
        lifetime: 180000,
        dataMimeType: 'application/json',
        metadataMimeType: 'message/x.rsocket.routing.v0',
    },
    transport: new RSocketWebSocketClient({
        url: wsURL
    })
});
// error handler
const errorHanlder = (e) => console.log(e);

// response handler
const responseHanlder = (payload) => {
    const li = document.createElement('li');
    li.innerText = payload.data;
    li.classList.add('list-group-item', 'small')
    document.getElementById('result').appendChild(li);
}

/////////
//  number.stream
////////
/*

const numberRequester = (socket, value) => {
    socket.requestStream({
        data: value,
        metadata: String.fromCharCode('number.stream'.length) + 'number.stream'
    }).subscribe({
        onError: errorHanlder,
        onNext: responseHanlder,
        onSubscribe: subscription => {
            subscription.request(100); // set it to some max value
        }
    })
}

client.connect().then(sock => {
    document.getElementById('n').addEventListener('change', ({srcElement}) => {
        numberRequester(sock, parseInt(srcElement.value));
    })
}, errorHanlder);
*/

/////////
//  number.channel
////////
// reactive stream processor
const processor = new FlowableProcessor(sub => {});

const numberRequester = (socket, processor) => {
    socket.requestChannel(processor.map(i => {
        return {
            data: i,
            metadata: String.fromCharCode('number.channel'.length) + 'number.channel'
        }
    })).subscribe({
        onError: errorHanlder,
        onNext: responseHanlder,
        onSubscribe: subscription => {
            subscription.request(100); // set it to some max value
        }
    })
}

client.connect().then(sock => {
    numberRequester(sock, processor);
    document.getElementById('n').addEventListener('keyup', ({srcElement}) => {
        if(srcElement.value.length > 0){
            processor.onNext(parseInt(srcElement.value))
        }
    })
}, errorHanlder);

安裝 node_modules

npm install

設定

npm run build

啟動 client

npm run serve

問題

一直找不到如何單獨使用 rsocket-js 的方法,就是不透過 nodejs 提供 網頁 service 的方法。現在很多 js library 只有用在 nodejs 的 sample,但傳統的網頁,應該只要透過 webserver 直接載入 javascript file,就可以使用才對。如果可以這樣用,就可以將 html, js 合併到 sping boot 裡面,也不需要兩個 service port。

References

RSocket 革命,為了 Reactive Programming 而生的高效率通訊協定

RSocket With Spring Boot

2022/05/23

Annotations in Spring Boot

Spring 在 2.5 以後,不用集中式的 xml 設定檔,改用 annotation 定義,傳統的 XML 設定冗長,改用 annotation 這個方法後有好有壞,這讓開發者專注在程式開發,但會有不知道為什麼這樣設定就能運作的錯覺。

以下是 Spring Application 能看到的 annotations

Core Spring Framework

@Autowired

可用在 fields, setter methods, constructors,能夠直接 inject object dependency

fields: 透過 property name 自動設定 field value

public class Customer {
    @Autowired                               
    private Person person;                   
    private int type;
}

setter: 告訴 Spring 要在 init bean 時,呼叫 setter method

public class Customer {                                                                                         
    private Person person;
    @Autowired                                                                                                      
    public void setPerson(Person person) {
       this.person=person;
    }
}

constructor: 產生物件時,做 injection,只能有一個 constructor 帶有 @Autowired annotation

@Component
public class Customer {
    private Person person;
    @Autowired
    public Customer (Person person) {
        this.person=person;
    }
}

@Qualifier

跟 @Autowired 一起使用,當該 @Bean 有多個 instance 時,可透過 @Qualifier 指定 instance

如果有兩個 BeanInterface 的 instances

@Component
public class BeanB1 implements BeanInterface {
    //
}
@Component
public class BeanB2 implements BeanInterface {
    //
}

透過 @Qualifier("beanB2") 指定 beanB2

@Component
public class BeanA {
    @Autowired
    @Qualifier("beanB2")
    private IBean dependency;
    ...
}

@Configuration

類似以往的 XML config file (applicationContext.xml),用來設定 Spring IoC beans

ex:

@Configuartion
public class DataConfig {
    @Bean
    public DataSource source() {
        DataSource source = new OracleDataSource();
        source.setURL();
        source.setUser();
        return source;
    }
    @Bean
    public PlatformTransactionManager manager() {
        PlatformTransactionManager manager = new BasicDataSourceTransactionManager();
        manager.setDataSource(source());
        return manager;
    }
}

@SpringBootApplication 本身就包含了 @Configuartion,可直接在裡面使用 @Bean

ex:

@SpringBootApplication
public class SpringBootBeanDemoApplication {

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

    @Bean
    public FooService fooService() {
        return new FooService();
    }

}

@ComponentScan

跟著 @Configuration 一起使用

告訴 Spring 去哪一個 package 掃描 annotated components

@ComponentScan("com.test.config")
@Configuration
public class AppConfig {
}

可以指定 basePackageClasses 或 basePackages,取代直接設定 package。這樣可避免 class package 異動時,還要修改 @ComponentScan 的 package name

@ComponentScan(basePackageClasses = ApplicationController.class)
@Configuration
public class AppConfig {
}

@Bean

只能用在 method,跟 @Configuration 一起使用,告訴 Spring 產生哪些 bean

@Configuration
public class AppConfig {
    @Bean
    public Person person() {
        return new Person(address());
    }
    @Bean
    public Address address() {
        return new Address();
    }
}

@Lazy

用在 component class

autowired dependencies 預設會在啟動時,自動 created & configured

加上 @Lazy,表示該 bean 會在第一次被使用時,才被 created

@Lazy 也可以用在 @Configuration classes,表示所有的 @Bean 都會被延遲建立

@Value

用在 field, constructor parameter, method parameter

表示該 field/parameter 的初始值

通常 Spring 會自動載入 application.properties 的設定,如果是自訂 properties file,要用 @PropertySource 指定

ex:

#application.properties
user.name=admin

#my.properties
user.password=pwd123
@PropertySource("classpath:my.properties")
public class ValueController {
    @Value("${user.name}")
    private String name;

    @Value("${user.password}")
    private String password;
}

也可以指定 List, Array

tools=car,train,airplane
@Value("${tools}")
private String[] toolArray;

@Value("${tools}")
private List<String> toolList;

Spring Framework Stereotype Annotations

@Component

用在 class,代表一個 Spring component

@Component 可讓 Java class 在 component scanning 時被放入 application context

@Controller

代表 Spring controller

用在 Spring MVC 或 Spring WebFlux 的 controller

@Service

用在 class,讓 java class 提供某個 service,ex: 執行 business logic、計算、呼叫外部 API,這是一種用在 service layer 特殊的 @Component

@Repository

用在 Java class 可直接存取 DB,也就是 Data Access Object 的角色

有 auto translation 功能,ex: 發生 exception 時,會有 handler 處理該 exception,不需要加上 try-catch block

Spring Boot Annotations

@EnableAutoConfiguration

用在 main application class

會自動定義一個 base "search package",告訴 Spring Boot,根據 classpath 設定, perperties 設定,加入beans

會檢查所有 sub-packages & class

@Configuration
@EnableAutoConfiguration
public class EmployeeApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(EmployeeApplication.class, args);
        // ...
    }
}

排除 JdbcTemplateAutoConfiguration.class

@Configuration
@EnableAutoConfiguration(exclude={JdbcTemplateAutoConfiguration.class})
public class EmployeeApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(EmployeeApplication.class, args);
        // ...
    }
}

@SpringBootApplication

用在 application class,設定 Spring Boot project

會自動掃描 sub-packages,ex:

會掃描 com.example 的所有 sub-packages

package com.example;

@SpringBootApplication
public class EmployeeApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(EmployeeApplication.class, args);
        // ...
    }
}

使用 @SpringBootApplication 就等於使用了

  • @Configuration
  • @EnableAutoConfiguration
  • @ComponentScan

Spring MVC and REST Annotations

@Controller

會自動偵測 classpath 的 component classes,並自動註冊 bean definitions

如果要偵測 annotated controllers,必須加入 component scanning 到 configuration。

@Controller 可處理多個 request mappings

@Controller 用在 Spring MVC 及 Spring WebFlux

@RequestMapping

可用在 class 及 method

用來 map web request 到某個 class / handler methods

用在 class level 時,會產生該 controler 的 base URI,然後就能使用所有 handler methods

如果要限制 HTTP method,可在 handler method 上加上 @RequestMapping,ex:

只接受 GET method request 發送到 /welcome

@Controller
@RequestMapping("/welcome")
public class WelcomeController {
    @RequestMapping(method = RequestMethod.GET)
    public String welcomeAll() {
        return "welcome all";
    }
}

用在 Spring MVC 及 Spring WebFlux

@CookieValue

用在 method parameter level

用在 request mapping method 的參數,透過 cookie name 綁定

ex:

當有個 http request 夾帶 JSESSIONID 的 cookie,可透過 @CookieValue 取得 JSESSIONID 的值

JSESSIONID=418AB76CD83EF94U85YD34W
@ReuestMapping("/cookieValue")
    public void getCookieValue(@CookieValue "JSESSIONID" String cookie){
}

@CrossOrigin

用在 class 及 method level,可 enable cross-origin requests

如果可讓 js 從不同 host 取得資料,就要用 @CrossOrigin 啟用 cross-origin resource sharing

@CrossOrigin 預設允許所有 origins, headers,maxAge 為 30 min

ex:

getMessage 與 getNot 都會是 maxAge of 3600 seconds

getMessage 只允許來自 http://example.com 的 request

getNote 允許所有 hosts

@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin(origins = "http://example.com")
    @RequestMapping("/message")
    public Message getMessage() {
        // ...
    }

    @RequestMapping("/note")
    public Note getNote() {
        // ...
    }
}

Composed @RequestMapping Variants

用在 Spring MVC 及 WebFlux

@GetMapping

只接受 Get method request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.GET)

@PostMapping

只接受 Postmethod request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.POST)

@PutMapping

只接受 PUT method request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.PUT)

@PatchMapping

只接受 Patch method request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.PATCH)

@DeleteMapping

只接受 DELETE method request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.DELETE)

@ExceptionHandler

處理 controller level 的 exception,定義該 class 要 catch 的 exception class

@ExceptionHandler 的 value 可以是 array of Expception types

@InitBinder

method-level

識別用來初始化 WebDataBinder 的 methods

WebDataBinder 是可將 request parameter 綁定 JavaBean objects 的 DataBinder

可在 controller 使用 @InitBinder annotated method

@InitBinder annotated methods 會在每一次 http request 被呼叫。用來驗證參數是否符合規則

ex: 如果有一個 request form 是 Student 資料

先產生 Student Java Bean

public class Student{
 private String Id;
 private String firstName;

 @NotNull(message="is required")
 @Size(min=1,message="is required")
 private String lastName; // validation done to check if lastname is NULL

 @Max(value=10,message="Value should between 0 and 10")
 @Min(value=0,message="Value should between 0 and 10")
 private String standard;
 private String Age;
}

用 initBinder 註冊 StringTrimmerEditor

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
  StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
    dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
}

@Mappings and @Mapping

用在 fields

@Mappings 是 web mapping annotation,可對應 source field 到 target field

@MaxtrixVariable

用在 request handler method arguments

可 inject relevant bits of a maxtri URI

maxtrix variables 會以 semicolon 區隔

如果 URL 包含 matrix variables,request mapping pattern 會以 URI template 表示

http://localhost:8080/spring/employees/John;beginContactNumber=22001

用以下 method

@RequestMapping(value = "/employees/{name}", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<List<Employee>> getEmployeeByNameAndBeginContactNumber(
  @PathVariable String name, @MatrixVariable String beginContactNumber) {
    List<Employee> employeesList = new ArrayList<Employee>();
    ...
    return new ResponseEntity<>(employeesList, HttpStatus.OK);
}

URI

http://localhost:8080/spring/employees/id=1;name=John;contactNumber=2200112334
@GetMapping("employees/{employee}")
@ResponseBody
public ResponseEntity<Map<String, String>> getEmployeeData(
  @MatrixVariable Map<String, String> matrixVars) {
    return new ResponseEntity<>(matrixVars, HttpStatus.OK);
}

@PathVariable

用在 method arguments

處理 dynamic changes in URI

可以用 regular expression

ex:

@GetMapping("/api/employees/{id}/{name}")
@ResponseBody
public String getEmployeesByIdAndName(@PathVariable String id, @PathVariable String name) {
    return "ID: " + id + ", name: " + name;
}

@RequestAttribute

綁定 request attribute 到 handler method paratmeter

ex: 計算 visit counter

Interceptor 增加 request attribute

public class MyCounterInterceptor extends HandlerInterceptorAdapter {
  private AtomicInteger counter = new AtomicInteger(0);

  @Override
  public boolean preHandle (HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) throws Exception {

      request.setAttribute("visitorCounter", counter.incrementAndGet());
      return true;
  }
}

controller

@Controller
public class ExampleController {

  @RequestMapping("/")
  @ResponseBody
  public String handle (@RequestAttribute("visitorCounter") Integer counter) {
      return String.format("Visitor number: %d", counter);
  }
}

JavaConfig

@EnableWebMvc
@ComponentScan("com.example")
public class AppConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addInterceptors (InterceptorRegistry registry) {
      registry.addInterceptor(new MyCounterInterceptor());
  }
}

ref: https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/request-attribute.html

@RequestBody

一般用於處理非 Content-Type: application/x-www-form-urlencoded 編碼格式的資料,例如:application/json、application/xml

@RequestMapping(path = "/something", method = RequestMethod.PUT)
public void handle(@RequestBody String body, Writer writer) throws IOException {
    writer.write(body);
}

@ResponseStatus(value = HttpStatus.OK)
@PostMapping(value="/user", consumes = MediaType.APPLICATION_JSON_VALUE)
public void process2(@RequestBody User user) {
  logger.info("User: {}", user);
}

@RequestHeader

將 request header 裡面的 name 對應到 handler method parameter

@RequestParam

在 controller 中,跟 @RequestMapping 一起使用,綁定 request parameter 到 method parameter

@RequestPart

取代 @RequestParam

可取得特定 multipart 的 content 到 method parameter

Content-type 必須要是 multipart

@ResponseBody

用在 handler methods,類似 @RequestBody

spring 會將物件透過 HttpMessageConverter 轉換為 JSON/XML 寫入response body

@ResponseStatus

用在 methods, exception class,指定回傳的 status code

@ControllerAdvice

class level

可定義@ExceptionHandler, @InitBinder, and @ModelAttribute methods ,就會套用在所有 @RequestMapping methods

如果在 @ControllerAdvice class 的某個 method 加上 @ExceptionHandler,會被套用在所有 controllers

@RestController

將 class 標記為 controller

使用 @RestController 就不需要在所有 RequestMapping methods 加上 @ResponseBody,這表示不會在 response 回傳 html

@RestControllerAdvice

等同 @ControllerAdvice 與 @ResponseBody,再加上 @ExceptionHandler 處理 exceptions

@SessionAttribute

method parameter level

綁定 method parameter to a session attribute

@SessionAttributes

type level

要跟 @ModelAttribute 一起使用

可以將 JavaBean object 增加到 session 中

@ModelAttribute("person")
public Person getPerson() {}
// within the same controller as above snippet
@Controller
@SeesionAttributes(value = "person", types = {
    Person.class
})
public class PersonController {}

Spring Cloud Annotations

@EnableConfigServer

class level

當 project 有多個 services,需要集中設定所有 services

centralized config server 的優點是不需要知道 components 在哪裏

@EnableEurekaServer

用在 class

使用 microservices 時,很難知道每一個 service 相關的 addresses,必須要有 service discovery 方法

Netflix’s Eureka 是 discovery server 的實作

@EnableDiscoveryClient

用在 java class

通知哪一個 application 要註冊到 Eureka,會將 host, port 註冊到 Eureka

@EnableCircuitBreaker

用在 java class

circuit breaker pattern 讓 microservice 在相關 service fails 還能持續運作,避免 error 擴散

讓 service 有 recover 的時間

@HystrixCommand

method level

Netflix’s Hystrix library 是 Circuit Breaker pattern 的實作

當 method 套用 circuit breaker,Hystrix 會監控 failure of the method。當錯誤發生,Hystrix 會打開 circuit,後續的呼叫會 fail,Hystrix 會 redirect calls 到這個 method

ex:

@Service
public class BookService {
    private final RestTemplate restTemplate;
    public BookService(RestTemplate rest) {
        this.restTemplate = rest;
    }
    @HystrixCommand(fallbackMethod = "newList") public String bookList() {
        URI uri = URI.create("http://localhost:8081/recommended");
        return this.restTemplate.getForObject(uri, String.class);
    }
    public String newList() {
        return "Cloud native Java";
    }
}

Spring Framework DataAccess Annotations

@Transactional

放在 interface 定義, interface 的 method, class 定義, class 的 public method 前面

@Transactional 只是一個 metadata,讓 beans 設定 tranactional behavior

Cache-Based Annotations

@Cacheable

用在 methods

可將 return 的資料以 addresses 儲存到 cache

@Cacheable("addresses")
public String getAddress(Book book){...}

每次該 method 被呼叫時,會先檢查 cache

@CachePut

當要 update cache 時,可用 @CachePut

@CachePut("addresses")
public String getAddress(Book book){...}

不建議將 @CachePut, @Cacheable 合併使用

@CacheEvict

用在 method

可將 cache 清空,allEntries 就是清除所有 values

@CacheEvict(value="addresses", allEntries="true")
public String getAddress(Book book){...}

@CacheConfig

class level

可儲存 cache configuration

Task Execution and Scheduling Annotations

@Scheduled

method level

該 method 必須回傳 void,且不能有任何參數

// 等 5s 後開始執行,會等前一次結束後再跑第二次
@Scheduled(fixedDelay=5000)
public void doSomething() {
    // something that should execute periodically   
}

// 每 5s 執行一次,不管前一次有沒有跑完
@Scheduled(fixedRate=5000)
public void doSomething2() { 
    // something that should execute periodically 
}

@Scheduled(initialDelay=1000,fixedRate=5000)
public void doSomething3() { 
   // something that should execute periodically after an initial delay  
}

@Async

method level

每個 method 都在不同 thread 執行

@Async 如果有 return value,必須要是 Future-typed

Spring Framework Testing Annotations

@BootstrapWith

class level

設定 Spring TestContext Framework 如何啟動

@ContextConfiguration

class level

宣告要載入 context 的 class,及設定的 xml

@ContextConfiguration(locations={"example/test-context.xml", loader = Custom ContextLoader.class})

@WebAppConfiguration

class level

ApplicationContext 要載入的必須要是 WebApplicationContext

@WebAppConfiguration 可產生 web version of the application context

default root of web app 為 src/main/webapp

@Timed

method

該method 必須在限定時間內執行結束。超時就代表測試失敗

@Timed(millis=10000)
public void testLongRunningProcess() {  ... }

@Repeat

用在 test method

重複執行

@Repeat(10)
@Test
public void testProcessRepeatedly() {  ... }

@Commit

class level or method level

執行後,transaction of the transactional test method 會被 commit

@RollBack

class level or method level

@Rollback(true), the transaction is rolled back

@DirtiesContext

class, method level

表示 ApplicationContext 已被 modified / corrupted

@BeforeTransaction

在 @Transactional 前執行

@AfterTransaction

在 @Transactional 後

@Sql

run SQL scripts against a database

@SqlConfig

決定 parse and execute SQL scripts 的方法

@SqlGroup

method level

a container annotation that can hold several @Sql annotations

@SpringBootTest

start the Spring context for integration tests

@DataJpaTest

test Spring Data JPA using an in-memory database such as H2

@DataMongoTest

@WebMVCTest

a mock servlet context for testing the MVC layer

@AutoConfigureMockMVC

@MockBean

產生並 inject Mockito Mock

@JsonTest

限制測試處理 JSON 的 auto-configuration of Spring Boot 的 components

@TestPropertySource

class level

property sources for the test class

References

A Guide to Spring Framework Annotations

太厲害了!終於有人把Spring Boot常用註釋講明白了!

Top 10 Spring Framework Annotations for Java Programmers

Spring Annotation Note

2022/05/16

GraphQL with SpringBoot & MySQL

這是使用 SpringBoot 並將資料放在 MySQL 的 GraphQL sample

建立專案

Spring Initializer 產生 maven project,在 Dependencies 的部分增加

  • Lombok
  • MySQL Driver
  • Spring Data JPA
  • Spring Web

取得 project 後,再增加 Graphql dependencies

              <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-spring-boot-starter</artifactId>
            <version>5.0.2</version>
        </dependency>
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-tools</artifactId>
            <version>5.2.4</version>
        </dependency>

Data Model

因應 MySQL 的 Table,需要先產生 ORM 的 Data Model

Post.java

package com.example.writeup.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "USER")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private Integer userId;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "DOB")
    private Date dob;

    @Column(name = "ADDRESS")
    private String address;

    @Column(name = "POST_ID")
    private Integer postId;

    public User(String firstName, String lastName, Date dob, String address, Integer postId) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
        this.address = address;
        this.postId = postId;
    }
}

User.java

package com.example.writeup.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "USER")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private Integer userId;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "DOB")
    private Date dob;

    @Column(name = "ADDRESS")
    private String address;

    @Column(name = "POST_ID")
    private Integer postId;

    public User(String firstName, String lastName, Date dob, String address, Integer postId) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
        this.address = address;
        this.postId = postId;
    }
}

Repository

這是用來跟資料庫建立跟剛剛的 Data Model 的關聯

PostRepository.java

package com.example.writeup.repository;

import com.example.writeup.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post,Integer> {
}

UserRepository.java

package com.example.writeup.repository;

import com.example.writeup.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

    @Transactional
    @Modifying
    @Query(value = "UPDATE user SET address = ?1 WHERE user_id = ?2 ", nativeQuery = true)
    int updateUserAddress(String address, Integer user_id);

}

Note: 原文說可以改為繼承 CrudRepository,但 find 會回傳一般的 List,但 JpaRepository 會回傳 iterable list ref: What is difference between CrudRepository and JpaRepository interfaces in Spring Data JPA?

DataLoader

用在 init project 時,會自動產生 MySQL table 及測試資料

DataLoader.java

package com.example.writeup.service;

import com.example.writeup.model.Post;
import com.example.writeup.model.User;
import com.example.writeup.repository.PostRepository;
import com.example.writeup.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;

@Service
public class DataLoader {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PostRepository postRepository;

    @PostConstruct
    public void loadData(){

        User user1 = new User("Yasas" ,"Sandeepa",DataLoader.getRandomDate(),"Mount Pleasant Estate Galle",1);
        User user2 = new User("Sahan" ,"Rambukkna",DataLoader.getRandomDate(),"Delkanda Nugegoda",2);
        User user3 = new User("Ranuk" ,"Silva",DataLoader.getRandomDate(),"Yalawatta gampaha",3);

        Post post1 = new Post("Graphql with SpringBoot",DataLoader.getRandomDate());
        Post post2 = new Post("Flutter with Firebase",DataLoader.getRandomDate());
        Post post3 = new Post("Nodejs Authentication with JWT",DataLoader.getRandomDate());

        postRepository.save(post1);
        postRepository.save(post2);
        postRepository.save(post3);

        userRepository.save(user1);
        userRepository.save(user2);
        userRepository.save(user3);
    }

    public static Date getRandomDate(){
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, 1990);
        calendar.set(Calendar.MONTH, 1);
        calendar.set(Calendar.DATE, 2);
        Date date1 = calendar.getTime();
        calendar.set(Calendar.YEAR, 1996);
        Date date2 = calendar.getTime();
        long startMillis = date1.getTime();
        long endMillis = date2.getTime();
        long randomMillisSinceEpoch = ThreadLocalRandom
                .current()
                .nextLong(startMillis, endMillis);

        return new Date(randomMillisSinceEpoch);
    }
}

application.properties

產生設定檔

server.port=7000

#mysql properties
spring.jpa.generate-ddl=true
spring.datasource.url=jdbc:mysql://localhost/writeup
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create

#graphql properties
graphql.servlet.corsEnabled=true
graphql.servlet.mapping=/graphql
graphql.servlet.enabled=true

GraphQL

  • 一開始的 pom.xml 已增加了相關 dependencies
  • 剛剛的 application.properties 增加 graphql.servlet 的設定
  • 建立 GraphQL schema

schema.graphqls

schema {
    query: Query,
    mutation: Mutation,
}

type Query{
    # Fetch All Users
    getAllUsers:[User]

}

type Mutation {
    # Update the user address
    updateUserAddress(userId:Int,address:String): User
}

type User {
    userId : ID!,
    firstName :String,
    lastName :String,
    dob:String,
    address:String,
    postId : Int,
}
  • 產生 Service

UserService.java

package com.example.writeup.service;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.example.writeup.model.User;
import com.example.writeup.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService implements GraphQLQueryResolver, GraphQLMutationResolver {

    @Autowired
    private UserRepository userRepository;

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    public User updateUserAddress(Integer userId, String address) throws Exception {
        try {
            userRepository.updateUserAddress(address, userId);
            User user = userRepository.findById(userId).get();
            return user;
        } catch (Exception e) {
            throw new Exception(e);
        }
    }
}

WriteupApplication

package com.example.writeup;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WriteupApplication {

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

}

Altair

這是 GraphQL 的測試工具,這邊安裝了 Chrome Extension 版本

網址為 http://localhost:7000/graphql/ ,點 Docs 可取得 Query 及 Mutation 兩個介面

References

[譯]使用 SpringBoot 和 MySQL 構建 GraphQL 服務端應用程式

Build a GraphQL Server With Spring Boot and MySQL

WriteUp github

Altair Altair GraphQL Client helps you debug GraphQL queries and implementations

2022/05/09

原生 vs 跨平台

現今軟體應用程式有四種使用者介面:終端機(Command Line Interface)、網頁、Mobile APP、Desktop APP。在為應用程式考量要提供哪一種使用者介面時,如果面對一般使用者,通常會以 網頁 -> Mobile -> Dekstop -> CLI 這樣的順序考慮。但每一種類型的使用者介面,又通常因為運作的 OS 平台不同而有不同的差異,導致開發的方法跟經驗,無法跨出熟悉的領域。

為了節省開發耗費的資源,跨平台成了開發者的首要考量,就如同 Java 一開始的宏願:Write Once, Run Everywhere!,這個目標,Java 並沒有取得全面的成功,但 Java 至少統一了過去數十年一部分的 Application 的開發。Java 在嵌入式環境、Mobile 行動平台、Desktop、網頁 Applet 直到現在都已經確認是失敗的。

失敗的重要原因在於效能及耗費的系統資源量,因為跨平台的要求,會需要將平台相關的功能包裝起來,再用另一個標準提供開發者介面,但這個封裝可能會造成系統資源的耗費,畢竟原生的函式庫會越貼近作業系統,資源的使用會比較少,效能也比較好。瀏覽器之間的差異相較於作業系統,差異是很小的,因此 Web 會是使用者介面的首選。

但為什麼還是會有跨平台的需求?畢竟不是每一個提供服務的廠商,都有足夠的工程師配額,能夠處理不同平台的開發。只要能夠節省成本,就永遠都會有跨平台的需求。

Web -> Mobile -> Dekstop -> CLI

一般使用者都能夠使用圖形介面 GUI 的應用程式,但終端機 Command Line Interface 就不同了,沒有經過 Linux OS 洗禮的使用者,通常無法適應一個字一個字敲打的指令列,通常也只有開發者才會去接觸 CLI 的程式。

在這幾種使用介面中,以網頁是最符合跨平台的開發方法,因為網頁的標準規格,各家瀏覽器都要遵循這些規格去開發,再加上有眾多框架與函式庫,解決了不同瀏覽器之間的些微差異問題,因此為應用提供網頁使用者介面,基本上是最常見的做法。

但實際上還是有很多應用程式,是沒辦法放在網頁裡面運作的,尤其是使用到 OS 原生的資源的應用。另外由於手機這種手持裝置的流行,人手一機,在面對一般使用者提供服務時,首先就會考慮提供手機 Mobile APP。

失敗經驗

過去曾經有使用過 Titanium 開發 iOS, Android 的失敗經驗,記得失敗的原因是 APP 只能做比較簡單的畫面,太複雜客製,非標準的 UI 元件,無法自由地透過 Titanium 產生,也無法產生具有不同平台特性的 UI 元件。

因此目前在 Mobile APP 比較常見的作法,是分開實作的,業界也分別需要 iOS 與 Android 的開發人員。也許當時我們是使用了一個封裝得不好的 Library 去開發,才導致失敗的結果。

不代表 iOS, Android 就一定要用原生開發,像 Game 的領域中,Unity 作為 Game Engine,也跨平台的 Game 開發做得很好。

Microsoft 也曾經想要統一 Desktop 與 Mobile 的使用者體驗,用 Tiles 做了 Windows Phone 及 Windows 10。現在大家已經知道,Windows Phone 已經完全在市場消失。而 Windows 11 的設計方式也已經向競爭對手看齊。

跨平台

是永久的需求及目標,成功與失敗的案例都有,但也都要經過嘗試,才能在一段時間後知道結果如何。

如果公司能夠負擔足夠的開發成本,或是專案運作的目標限制在某些平台上,當然就在不同平台去開發。要使用跨平台,如果能先用小的專案去測試,並先了解問題點,做先行測試,會是比較安全的作法。跨平台的效能跟資源耗費,必須要在可接受的範圍內。

另外也要考慮平台升級的問題,一但使用了跨平台的函式庫,就表示當核心升級後,會需要等待一段時間,才能取得跨平台函式庫的更新,這一段時間會是空窗期,遇到問題是完全無法解決的,也只能靜靜等待更新與修正。

References

2021 年加速開發的 8 個最佳跨平台框架

放弃坚持 15 年的原生开发,1Password 用 Electron 重写了全部代码

Top 10 Best Cross Platform App Development Frameworks in 2021