javascript
使用Java和JSF构建一个简单的CRUD应用
使用Okta的身份管理平臺輕松部署您的應(yīng)用程序 使用Okta的API在幾分鐘之內(nèi)即可對任何應(yīng)用程序中的用戶進行身份驗證,管理和保護。 今天嘗試Okta。
JavaServer Faces(JSF)是用于構(gòu)建Web應(yīng)用程序的Java框架,其中心是作為用戶界面構(gòu)建塊的組件。 JSF受益于豐富的工具和供應(yīng)商生態(tài)系統(tǒng),以及開箱即用的組件和庫,它們增加了更多的功能。
為什么使用JSF代替JavaServer Pages(JSP)? 主要有兩個原因:首先,JSF具有更多的模板功能,因為它遇到標簽時不會直接編寫視圖。 相反,您可以將視圖構(gòu)建為XML,然后可以對其進行預(yù)處理,然后再將其輸出為HTML。 這意味著您可以隨著應(yīng)用程序的增長重新使用并更好地組織代碼。 其次,JSF為您提供了完整的MVC架構(gòu),而JSP只是一種腳本語言,它抽象了手工編寫Servlet的過程。
在此背景下,讓我們創(chuàng)建一個簡單的應(yīng)用程序,以展示JSF的強大功能。 在本教程中,我們將構(gòu)建一個簡單的Web應(yīng)用程序,以管理由數(shù)據(jù)庫支持的您喜歡的書籍的列表,并使用Okta安全地訪問您的應(yīng)用程序。
使用JSF創(chuàng)建CRUD應(yīng)用程序
首先,我們將使用TomEE Maven原型生成項目:
$ mvn archetype:generate \-DarchetypeGroupId=org.apache.openejb.maven \-DarchetypeArtifactId=tomee-webapp-archetype \-DarchetypeVersion=1.7.1遵循交互式生成過程,使用以下參數(shù)生成應(yīng)用程序:
Define value for property 'groupId': com.okta.developer Define value for property 'artifactId': jsf-crud Define value for property 'version' 1.0-SNAPSHOT: : 1.0-SNAPSHOT Define value for property 'package' com.okta.developer: : com.okta.developer Confirm properties configuration: groupId: com.okta.developer artifactId: jsf-crud version: 1.0-SNAPSHOT package: com.okta.developer Y: : Y然后cd轉(zhuǎn)化項目,構(gòu)建和運行,看到它在行動:
$ cd jsf-crud # or the artifactId you used to generate the project $ mvn package $ mvn tomee:run在您的JSF應(yīng)用程序中創(chuàng)建一本書
現(xiàn)在,將您的首選瀏覽器指向http://localhost:8080/ 。 您應(yīng)該看到用于創(chuàng)建書籍的表單。
要添加書籍,只需輸入書名,然后點擊添加即可 。 您將進入成功頁面,并能夠查看數(shù)據(jù)庫中的書籍列表。 此表單頁面由src/main/webapp/book.xhtml生成,結(jié)果頁面由src/main/webapp/result.xhtml 。
book.xhtml是一個簡單的表單,將其字段連接到com.okta.developer.presentation.BookBean類。 例如:
<h:inputText value='#{bookBean.bookTitle}'/>對于操作,例如提交表單,我們直接綁定到“ bean”類中的方法。 該特定形式將觸發(fā)類中的add()方法:
<h:commandButton action="#{bookBean.add}" value="Add"/>BookBean.add()方法創(chuàng)建一個新的Book實例,并將其標題設(shè)置為我們存儲在bookTitle字段中的bookTitle (請記住,它已綁定到表單的輸入字段):
public String add() {Book book = new Book();book.setBookTitle(bookTitle);bookService.addBook(book);return "success"; }然后,它要求bookService將圖書持久bookService到數(shù)據(jù)庫中,就像我們在com.okta.developer.application.BookService類中看到的com.okta.developer.application.BookService :
public void addBook(Book book) {entityManager.persist(book); }但是返回的"success"字符串呢? 它在文件src/main/webapp/WEB-INF/faces-config.xml :
<navigation-rule><from-view-id>/book.xhtml</from-view-id><navigation-case><from-outcome>success</from-outcome><to-view-id>/result.xhtml</to-view-id></navigation-case> </navigation-rule>這意味著當/book.xhtml文件success時,JSF會將用戶發(fā)送到視圖/result.xhtml 。 在result.xhtml文件中,我們還看到一個綁定到Bean中的方法的按鈕:
<h:commandButton action="#{bookBean.fetchBooks}" value="View books present"/>這將執(zhí)行類中的方法fetchBooks() 。 在src/main/java/com/okta/developer/presentation/BookBean.java文件中,我們看到fetchBooks()委托給bookService的方法,該方法將結(jié)果存儲到booksAvailable字段中,然后返回字符串“ success” 。
public String fetchBooks() {booksAvailable=bookService.getAllBooks();return "success"; }閱讀您的JSF應(yīng)用程序中的書
在getAllBooks()方法中, com.okta.developer.application.BookService類查詢數(shù)據(jù)庫,以獲取所有不帶過濾器的Book實例:
public List<Book> getAllBooks() {CriteriaQuery<Book> cq = entityManager.getCriteriaBuilder().createQuery(Book.class);cq.select(cq.from(Book.class));return entityManager.createQuery(cq).getResultList(); }涼。 但是頁面實際上如何顯示圖書信息? 在result.xhtml文件中,找到ui:repeat標簽:
<ui:repeat value="#{bookBean.booksAvailable}" var="book">#{book.bookTitle} <br/> </ui:repeat><ui:repeat>標記迭代每個value ,在這種情況下, #{bookBean.booksAvailable}是我們剛剛從fetchBooks()方法分配的字段。 集合的每個元素都可以通過標簽的var屬性中的名稱來引用(在本例中為book )。
<ui:repeat>標記內(nèi)的所有內(nèi)容都將對集合中的每個元素重復(fù)。 在這里,它只使用插值符號#{book.bookTitle}和換行符( <br/> )輸出書名。
我們只介紹了CRUD應(yīng)用程序的C reate和R ead方法。 太棒了! 現(xiàn)在讓我們嘗試與U PDATE一本書。
更新記錄
在src/main/webapp文件夾中創(chuàng)建一個edit.xhtml文件,以包含用于更新數(shù)據(jù)庫中Book的表單。 它看起來與“創(chuàng)建”表單非常相似:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"xmlns:f="http://java.sun.com/jsf/core"xmlns:h="http://java.sun.com/jsf/html"><h:body bgcolor="white"><f:view><h1>Update book</h1><h:form><h:panelGrid columns="2"><h:outputText value='Enter book title'/><h:inputText value='#{bookBean.bookTitle}'/><h:outputText value='Update'/><h:commandButton action="#{bookBean.update}" value="Update"/></h:panelGrid><input type="hidden" name="bookId" value='#{param.bookId}'/></h:form></f:view> </h:body> </html>現(xiàn)在,通過更改<ui:repeat>標記的內(nèi)容,從文件src/main/webapp/result.xhtml的書籍列表中添加指向此頁面的鏈接:
<ui:repeat value="#{bookBean.booksAvailable}" var="book"><h:link value="#{book.bookTitle}" outcome="edit"><f:param name="bookId" value="#{book.bookId}"/></h:link><br/> </ui:repeat>現(xiàn)在,列表中的每本書都是該書編輯頁面的鏈接。
但是,如果您嘗試單擊該鏈接,則會看到該表單在頁面加載時顯示為空。 讓我們解決這個問題,并在表單呈現(xiàn)之前加載書名。 要從我們的bean中的URL獲取bookId ,請在pom.xml文件中包括以下依賴項:
<dependencies>...<dependency><groupId>javax</groupId><artifactId>javaee-web-api</artifactId><version>6.0</version><scope>provided</scope></dependency>... </dependencies>編輯src/main/java/com/okta/developer/application/BookService.java文件,并將以下方法添加到BookService類以從數(shù)據(jù)庫中加載書籍:
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Root;...public Book getBook(Integer bookId) {CriteriaBuilder cb = entityManager.getCriteriaBuilder();CriteriaQuery<Book> cq = cb.createQuery(Book.class);Root<Book> book = cq.from(Book.class);cq.select(book);cq.where(cb.equal(book.get("bookId"), bookId));return entityManager.createQuery(cq).getSingleResult(); }并將以下邏輯添加到BookBean以在頁面渲染之前加載書籍:
import javax.annotation.PostConstruct; import javax.faces.context.FacesContext;...private Integer bookId; private Book book;@PostConstruct public void postConstruct() {String bookIdParam = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("bookId");if (bookIdParam != null) {bookId = Integer.parseInt(bookIdParam);book = bookService.getBook(bookId);bookTitle = book.getBookTitle();} }現(xiàn)在,讓我們創(chuàng)建一個將更改保存到數(shù)據(jù)庫的方法,在BookService類中使用以下方法:
public void update(Book book) {entityManager.merge(book); }另外,將update方法添加到BookBean類中:
public String update() {book.setBookTitle(bookTitle);bookService.update(book);return "success"; }為了正確地將用戶重定向到列表頁面,請將以下導航規(guī)則添加到文件src/main/webapp/faces-config.xml :
<navigation-rule><from-view-id>/edit.xhtml</from-view-id><navigation-case><from-outcome>success</from-outcome><to-view-id>/result.xhtml</to-view-id></navigation-case> </navigation-rule>現(xiàn)在我們已經(jīng)完成了書的更新,讓我們繼續(xù)進行“刪除”部分。
刪除記錄
上一節(jié)介紹了最困難的部分-在bean中裝入一本書。 添加刪除按鈕將更加容易。
將列表中每個條目的刪除鏈接添加到文件src/main/webapp/result.xhtml的編輯頁面:
<ui:repeat value="#{bookBean.booksAvailable}" var="book"><h:link value="#{book.bookTitle}" outcome="edit"><f:param name="bookId" value="#{book.bookId}"/></h:link><!-- Delete link: --><h:outputText value=" ("/><h:link value="Delete" outcome="delete"><f:param name="bookId" value="#{book.bookId}"/></h:link><h:outputText value=")"/><br/> </ui:repeat>現(xiàn)在,在src/main/webapp/delete.xhtml創(chuàng)建刪除確認頁面:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"xmlns:f="http://java.sun.com/jsf/core"xmlns:h="http://java.sun.com/jsf/html"><h:body bgcolor="white"><f:view><h1>Delete book?</h1><h:form><h:panelGrid columns="2"><h:outputText value='Book title'/><h:inputText value='#{bookBean.bookTitle}' readonly="true"/><h:outputText value='Delete'/><h:commandButton action="#{bookBean.delete}" value="Confirm Delete"/></h:panelGrid><input type="hidden" name="bookId" value='#{param.bookId}'/></h:form></f:view> </h:body> </html>并在BookBean類中添加適當?shù)膭h除處理程序:
public String delete() {bookService.delete(book);return "success"; }接下來,在BookService類中處理刪除:
import javax.persistence.Query;...public void delete(Book book) {Query query = entityManager.createQuery("DELETE FROM Book b WHERE b.bookId = :bookId");query.setParameter("bookId", book.getBookId());query.executeUpdate(); }并且不要忘記在刪除后通過將以下內(nèi)容添加到src/main/webapp/faces-config.xml將您的用戶重定向回列表:
<navigation-rule><from-view-id>/delete.xhtml</from-view-id><navigation-case><from-outcome>success</from-outcome><to-view-id>/result.xhtml</to-view-id></navigation-case> </navigation-rule>做完了! 我告訴你刪除會更容易。
所以,現(xiàn)在我們可以在c reate,U PDATE,R EAD和d elete我們的書籍的CRUD應(yīng)用程序。
改善用戶界面
CRUD應(yīng)用程序工作正常,但該應(yīng)用程序看起來不太好。 讓我們通過PrimeFaces改善我們的應(yīng)用程序用戶界面。
首先,將其作為依賴項添加到我們的pom.xml :
<dependencies>...<dependency><groupId>org.primefaces</groupId><artifactId>primefaces</artifactId><version>7.0</version></dependency>... </dependencies>現(xiàn)在,我們可以在視圖中使用任何PrimeFaces組件,方法是將其命名空間聲明為html標記,如下所示:
<html xmlns="http://www.w3.org/1999/xhtml"xmlns:f="http://java.sun.com/jsf/core"xmlns:h="http://java.sun.com/jsf/html"xmlns:p="http://primefaces.org/ui">有關(guān)更深入的概述,請閱讀其站點中的每個PrimeFaces組件 。
首先,讓我們從BookBean類中刪除圖書清單,然后創(chuàng)建一個BookList類。 頁面加載后,這將立即加載書單。 使用以下內(nèi)容創(chuàng)建文件src/main/java/com/okta/developer/presentation/BookList.java :
package com.okta.developer.presentation;import com.okta.developer.application.BookService; import com.okta.developer.entities.Book;import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Named; import java.util.List;@Named public class BookList {@Injectprivate BookService bookService;private List<Book> booksAvailable;@PostConstructpublic void postConstruct() {booksAvailable = bookService.getAllBooks();}public List<Book> getBooksAvailable() {return booksAvailable;} }從BookBean類中刪除以下與booksAvailable字段相關(guān)的代碼塊:
private List<Book> booksAvailable;public List<Book> getBooksAvailable() {return booksAvailable; }public void setBooksAvailable(List<Book> booksAvailable) {this.booksAvailable = booksAvailable; }public String fetchBooks() {booksAvailable=bookService.getAllBooks();return "success"; }我們還要更改目標網(wǎng)頁。 讓我們提供書單,而不是通過表單添加一本書。 為此,編輯index.jsp以將重定向更改為result.jsf :
<%@ page session="false" %> <%response.sendRedirect("result.jsf"); %>這是我的文件現(xiàn)在的樣子。 瀏覽PrimeFaces組件庫時 ,請隨時進行調(diào)整。
文件: src/main/webapp/book.xhtml使用p:panel和p:panelGrid :
<?xml version="1.0" encoding="UTF-8"?> <!-- File: book.xhtml --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"xmlns:h="http://java.sun.com/jsf/html"xmlns:p="http://primefaces.org/ui"> <h:head></h:head> <h:body><h:form><p:panel header="Create Book"><p:panelGrid columns="1" layout="grid"><p:outputLabel for="book-title" value="Enter book title"/><p:inputText id="book-title" value='#{bookBean.bookTitle}'/><p:commandButton value="Create" action="#{bookBean.add}" ajax="false"/></p:panelGrid><!-- We will use this later<input type="hidden" value="${_csrf.token}" name="${_csrf.parameterName}"/>--></p:panel></h:form> </h:body> </html>文件: src/main/webapp/delete.xhtml使用p:panel和p:panelGrid :
<?xml version="1.0" encoding="UTF-8"?> <!-- File: delete.xhtml --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"xmlns:h="http://java.sun.com/jsf/html"xmlns:p="http://primefaces.org/ui"> <h:head></h:head> <h:body><h:form><p:panel header="Delete Book?"><p:panelGrid columns="1" layout="grid"><p:outputLabel for="book-title" value="Book title"/><p:inputText id="book-title" value='#{bookBean.bookTitle}' readonly="true"/><p:commandButton value="Confirm Delete" action="#{bookBean.delete}" ajax="false"/></p:panelGrid><input type="hidden" name="bookId" value='#{param.bookId}'/><!-- We will use this later<input type="hidden" value="${_csrf.token}" name="${_csrf.parameterName}"/>--></p:panel></h:form> </h:body> </html>文件: src/main/webapp/edit.xhtml使用p:panel和p:panelGrid :
<?xml version="1.0" encoding="UTF-8"?> <!-- File: edit.xhtml --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"xmlns:h="http://java.sun.com/jsf/html"xmlns:p="http://primefaces.org/ui"> <h:head></h:head> <h:body><h:form><p:panel header="Update Book"><p:panelGrid columns="1" layout="grid"><p:outputLabel for="book-title" value="Enter new book title"/><p:inputText id="book-title" value='#{bookBean.bookTitle}'/><p:commandButton value="Update" action="#{bookBean.update}" ajax="false"/></p:panelGrid><input type="hidden" name="bookId" value='#{param.bookId}'/><!-- We will use this later<input type="hidden" value="${_csrf.token}" name="${_csrf.parameterName}"/>--></p:panel></h:form> </h:body> </html>文件: src/main/webapp/result.xhtml使用p:dataList而不是ui:repeat :
<?xml version="1.0" encoding="UTF-8"?> <!-- File: result.xhtml --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"xmlns:f="http://java.sun.com/jsf/core"xmlns:h="http://java.sun.com/jsf/html"xmlns:p="http://primefaces.org/ui"> <h:head></h:head> <h:body><h:link outcome="book" value="Create Book"/><p:dataList value="#{bookList.booksAvailable}" var="book" type="ordered"><f:facet name="header">Book List</f:facet>#{book.bookTitle}<h:outputText value=" ("/><p:link value="Edit" outcome="edit"><f:param name="bookId" value="#{book.bookId}"/></p:link><h:outputText value=" | "/><p:link value="Delete" outcome="delete"><f:param name="bookId" value="#{book.bookId}"/></p:link><h:outputText value=")"/></p:dataList> </h:body> </html>在啟用PrimeFaces的情況下運行應(yīng)用程序
使用mvn package tomee:run重新啟動應(yīng)用程序。 該應(yīng)用現(xiàn)在看起來會更好一些! 查看書籍清單:
使用Okta保護您的應(yīng)用程序
目前,任何人都可以訪問我們出色的Book應(yīng)用程序并更改數(shù)據(jù)庫。 為避免這種情況,讓我們使用Spring Security庫在應(yīng)用程序中添加一個安全層,并通過Okta對用戶進行身份驗證。
首先,立即注冊一個永久免費的開發(fā)者帳戶! 完成后,請完成以下步驟以創(chuàng)建OIDC應(yīng)用程序。
- http://localhost:8080/login/oauth2/code/okta
現(xiàn)在,使用您的Client ID和Client Secret創(chuàng)建文件src/main/resources/application.properties ,您可以在剛創(chuàng)建的應(yīng)用程序的General選項卡上找到它們。
okta.client-id={clientId} okta.client-secret={clientSecret} okta.issuer-uri=https://{yourOktaDomain}/oauth2/default讓我們在您的pom.xml文件中添加Spring Security作為依賴項:
<properties>...<spring-security.version>5.1.6.RELEASE</spring-security.version><spring.version>5.1.6.RELEASE</spring.version> </properties><dependencyManagement><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-framework-bom</artifactId><version>${spring.version}</version><scope>import</scope><type>pom</type></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-bom</artifactId><version>${spring-security.version}</version><scope>import</scope><type>pom</type></dependency></dependencies> </dependencyManagement><dependencies>...<dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-client</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>2.9.9</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.9.3</version></dependency> </dependencies>為了讓Spring Security正確地控制您的應(yīng)用程序安全,它需要知道哪些請求需要身份驗證。 為此,我們將創(chuàng)建文件src/main/java/com/okta/developer/SecurityConfiguration.java來告訴Spring Security保護所有URL并使用CSRF令牌保護表單:
public SecurityConfiguration(@Value("${okta.issuer-uri}") String issuerUri,@Value("${okta.client-id}") String clientId,@Value("${okta.client-secret}") String clientSecret) {this.issuerUri = issuerUri;this.clientId = clientId;this.clientSecret = clientSecret;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.sessionManagement() // Always create a session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)接下來,創(chuàng)建文件src/main/java/com/okta/developer/SecurityWebApplicationInitializer.java類以在應(yīng)用程序中啟用Spring Security:
package com.okta.developer;import org.springframework.security.web.context.*;public class SecurityWebApplicationInitializerextends AbstractSecurityWebApplicationInitializer {public SecurityWebApplicationInitializer() {super(SecurityConfiguration.class);} }由于已啟用CSRF保護,因此需要將令牌添加到每個<h:form>標簽。 只需在表單中添加該行(我在使用PrimeFaces的文件中將那些注釋掉了):
<input type="hidden" value="${_csrf.token}" name="${_csrf.parameterName}"/>做完了! 轉(zhuǎn)到http://localhost:8080 ,您將被重定向到Okta登錄表單,并且僅在成功通過身份驗證后才能使用該應(yīng)用程序。
想要與朋友分享應(yīng)用程序? 太酷了,轉(zhuǎn)到Okta開發(fā)人員控制臺頁面,轉(zhuǎn)到“ 用戶”并為其創(chuàng)建一個帳戶。 現(xiàn)在,您還具有功能齊全的安全管理工具,您可以在其中啟用/禁用用戶,檢查用戶何時登錄到您的應(yīng)用程序,重置其密碼等。
享受您的全新安全書本應(yīng)用程序吧!
了解有關(guān)Java,JSF和用戶身份驗證的更多信息!
如果您想使用Okta學習有關(guān)Java,JSF和用戶身份驗證的更多信息,我們還有其他很棒的文章供您繼續(xù)閱讀:
- 使用Java EE和OIDC構(gòu)建Java REST API
- 您應(yīng)該使用哪個Java SDK?
- 教程:用Java創(chuàng)建和驗證JWT
- 以第一語言學習Java
有什么問題嗎 要求未來的職位? 將它們放入評論中! 并且不要忘記在Twitter上關(guān)注@oktadev并在Youtube 上進行訂閱 。
該應(yīng)用程序的完整源代碼可在GitHub上找到: oktadeveloper / okta-java-jsf-crud-example 。
使用Okta的身份管理平臺輕松部署您的應(yīng)用程序 使用Okta的API在幾分鐘之內(nèi)即可對任何應(yīng)用程序中的用戶進行身份驗證,管理和保護。 今天嘗試Okta。
翻譯自: https://www.javacodegeeks.com/2019/11/build-a-simple-crud-app-with-java-and-jsf.html
總結(jié)
以上是生活随笔為你收集整理的使用Java和JSF构建一个简单的CRUD应用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: lucene自动补全_使用自动机的Luc
- 下一篇: 中国最北端的城市是哪个城市 中国最北端的
