Teyru

框架:Spring 形狀的容器與 web 層

依賴注入與請求綁定都在編譯期做完,而不是像 Spring 那樣在啟動時用反射掃描。

lib/17_container.teyrulib/18_web.teyru 加上 internal/sema/framework*.go 構成 一個應用程式框架,介面照 Spring 抄,實作方式不同。

為什麼不是反射

Spring 在啟動時掃描 classpath、讀註解、用反射建立與注入 bean、把請求綁到方法。 Teyru 沒有反射——但它在編譯期知道整個程式:每個類別、每個欄位、每個建構子、 每條繼承邊。所以這些工作在編譯期做完,而不是啟動時做完。

兩邊的差別是具體的:

SpringTeyru
bean 從哪來執行期掃描 classpath編譯器列舉所有 @Component 等標註的類別
注入誰執行期依型別查詢編譯期解析成 bean 名稱(ctx.getBean("userRepo")
缺少 beanNoSuchBeanDefinitionException,啟動時TY-TYP-0103,編譯期,附原始碼位置
循環相依BeanCurrentlyInCreationException,啟動時TY-TYP-0107,編譯期,印出環
執行期新增 bean可以不行
啟動成本掃描與反射沒有——註冊表是靜態初始化填好的

代價是明確的:不能執行期擴充。好處也是:整類錯誤從「部署之後才看到」變成「編譯 不過」。

容器

import teyru.*

@Repository
class UserRepo {
  String find(String id) { return "user " + id }
}

@Service
class UserService {
  @Autowired UserRepo repo
  @Value("${app.name:unnamed}") String appName
  private boolean ready = false

  @PostConstruct
  void start() { ready = true }

  String describe() { return appName + ": " + repo.find("7") }
}

class Main {
  public static void main(String[] args) {
    ApplicationContext ctx = new ApplicationContext()
    ctx.setProperty("app.name", "demo")
    ctx.refresh()
    UserService s = (UserService) ctx.getBean(UserService.class)
    System.out.println(s.describe())
  }
}

支援的註解

註解意思
@Component@Service@Repository@Controller@RestController這個類別是 bean;名稱預設是首字母小寫的類別名,@Component("x") 可改
@Configuration + @Bean這個類別本身也是 bean,它的 @Bean 方法各產生一個 bean
@Autowired欄位、建構子參數或 @Bean 方法參數由容器提供
@Qualifier("name")同型別有多個時指名
@Primary同型別有多個時的預設選擇
@Value("${key}")@Value("${key:default}")注入屬性
@PostConstruct建好並注入後呼叫(無參、回 void)
@Scope("prototype")每次查詢都建新的;預設 singleton

建構子選擇

Spring 4.3 起的規則:只有一個建構子就用它,否則找標了 @Autowired 的,否則用無參 的那個。標了多個 @Autowired,或都沒有而有多個又沒有無參建構子時,是 TY-TYP-0105

生命週期

refresh() 會把每個 singleton 都建起來,所以建構與注入的失敗會在啟動時(而不是 第一次請求時)報出來——缺 bean 更早就擋掉了,那是編譯期的 TY-TYP-0103。bean 的 建立是遞迴的:要 A 就先建它需要的 B。creating 旗標擋住環。

@PostConstruct 由產生的注入器在注入完成後呼叫。@PreDestroy 已宣告但沒有 執行——Teyru 沒有行程關閉鉤子,容器也沒有 close()

Web 層

import teyru.*

@RestController
@RequestMapping("/pets")
class PetController {
  @Autowired PetStore store

  @GetMapping("/{id}")
  Pet one(@PathVariable("id") int id) { return store.find(id) }

  @GetMapping("")
  String list(@RequestParam(value = "q", defaultValue = "all") String q) {
    return "q=" + q
  }

  @PostMapping("")
  Pet create(@RequestBody String body) { return new Pet(body, 1) }
}
註解意思
@RequestMapping(類別上)路徑前綴
@RequestMapping(方法上)路徑與 method(不寫是任何動詞)
@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping路徑與動詞
@PathVariable路徑裡 {name} 的值,會轉成參數的型別
@RequestParam查詢參數,defaultValue 可給預設
@RequestHeader請求標頭(名稱不分大小寫);defaultValue 已宣告但沒有作用,缺標頭時是空字串
@RequestBody請求主體;參數是 String 就原樣拿到,是類別(或 record)就以 Gson 綁定解析
@ResponseBody已宣告;@RestController 本來就隱含,所以有沒有都一樣

參數轉型:路徑與查詢參數都是字串,所以 intlongdoublefloatshortbyteboolean 的參數會經由對應的 parseX 轉換(intInteger.parseInt)。

請求主體@RequestBody 的參數是 String 時拿到原樣的主體(想自己看的 payload 就是這樣接),是類別或 record 時由編譯器為該型別產生的 Gson 綁定解析 ——Spring 依 Content-Type 挑訊息轉換器,這裡型別在編譯期就知道了,轉換器就是那 個綁定。解析失敗丟的是同一個 JsonSyntaxException。沒有 JSON 映射的型別是編譯 錯誤,不是第一次請求時的例外。

回應:回傳 HttpResponse 就完全自己決定;回傳 Stringtext/plain;回傳 void 是空主體;其他類別(含 record 與 enum,enum 寫成常數名稱的字串)以 Gson 綁定序列化成 application/json(見 docs/json.md)。基本型別沒有映射,跟陣列、 List 一樣是 TY-TYP-0111

路由Router.match 取最特定的符合——字面片段勝過變數片段,所以 /pets/mine 不會被 /pets/{id} 吃掉,與註冊順序無關。路徑存在但動詞不對是 405, 路徑不存在是 404。

伺服器

ApplicationContext ctx = Application.boot(args)
Router router = Application.routerFrom(ctx)
HttpServer server = new HttpServer(port, router, ctx)

HttpServer.handle(HttpRequest) 是請求進來後唯一的入口——它與 socket 迴圈分開, 所以不需要連線就能測:tests/programs/t102_web.teyrutests/programs/t141_web_param_errors.teyru 就是這樣測的,後者涵蓋轉型失敗的 400、 enum 參數與回傳值、defaultValue

已知限制

  1. 一次處理一個連線。 語言沒有執行緒(docs/language.md §13),所以第二個連線 要等第一個處理完。這對「會回應請求的程式」夠用,對「服務一群人」不夠;形狀已經 是 thread-per-connection 需要的形狀。
  2. 沒有內容協商。 只看方法的宣告型別,不看 Accept
  3. 回傳陣列、List 或基本型別還沒有 JSON 映射TY-TYP-0111),因為綁定尚未 支援它們。
  4. 只有 Application.boot--key=value 形式的命令列參數寫進屬性;沒有 application.properties 檔案的讀取。
  5. @PreDestroy 不執行;沒有 @Conditional@Profile@Import@Lazy、AOP、交易、@ExceptionHandler
  6. 沒有 @ComponentScan 的範圍控制:整個程式都是掃描範圍,因為編譯器看得見 全部——要排除什麼,就不要標註它。

本頁內容