扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
引言
目前,门户用户已习惯于在一个页面上包含多个 Portlet,习惯于在窗口框架(可提供控制图标或链接来更改 Portlet 模式或窗口状态)内嵌入每个 Portlet 的内容。WebSphere Application Server V6.1 提供了一组扩展功能,可用来通过 URL Addressability 自定义 Portlet,也可用来创建简单的门户框架。
要设计自定义门户框架,需要创建 JSP。通过引用新 JSP 标记库,可以方便地聚合 Portlet。这个聚合标记库允许您在窗口框架内呈现 Portlet 内容,或将多个 Portlet 聚合到一个页面上。
为了管理 Portlet,门户框架需要知道系统中提供了哪些 Portlet。WebSphere Application Server V6.1 提供了用于 Portlet 和 Portlet 应用程序的新托管 Bean (MBean)。您可以使用这些 MBean 来访问任何已部署 Portlet 的部署描述符中的信息。
Portlet URL Addressablity 本身就支持 HTML。如果您希望支持其他标记,可以使用 PortletDocumentFilters 来方便地添加不同的标记支持。与 Servlet 筛选器类似,将在 Portlet 前调用它们来更改行为或缺省输出。
本文将向您说明如何使用聚合标记库来构建自己的门户框架、如何使用 MBean API,以及如何自定义 Portlet 来支持其他标记。本文还将讨论这些功能与 WebSphere Portal 中类似功能的比较,以便了解二者的差异以及如何在这两个产品中实现相同的结果。
|
关于示例
您可以下载本文中描述的示例的示例代码,以便能够在阅读本文的过程中将其作为参考。各个示例均基于本系列文章的第 1 部分中讨论的 WorldClock Portlet 应用程序;为了便于您下载,示例代码包括在下载部分中。有关 WorldClock Portlet 的详细描述,请参见文章“将 WorldClock portlet 从 IBM Portlet API 转换到 JSR 168 portlet API”(请参见参考资料)。
|
聚合多个 Portlet
门户框架可以对门户页面上的 Portlet 内容进行管理、组织和排列。通过 URL Addressability,WebSphere Application Server 提供了门户页显示单个 Portlet 的最简单方法,无需使用任何图标或菜单链接。您可以通过使用新 JSP 标记库来方便地扩展此功能。此聚合库提供了一种方法来创建简单的自定义门户框架,用以处理 Portlet 操作和将多个 Portlet 呈现于窗口框架中,同时提供 Portlet 标题和用于更改 Portlet 模式或窗口状态的控制链接。
此库包含两类标记:
例如,此上下文定义 Portlet URL 的 URL 格式以及如何处理 Portlet 首选项。与 Servlet 不同,Portlet 必须始终呈现在门户框架的上下文中。
init
标记指定门户框架的 URL,以便 Portlet 的 URL 引用门户。您还可以将其他请求参数与门户 JSP 一起使用,以便对流进行控制。
<init portletURLPrefix="<portal URL>" portletURLSuffix="<portal specific URL suffix>" portletURLQueryParams="<portal specific query parameters"> |
由于 Portlet 需要使用门户上下文,因此必须将这些 Portlet 标记嵌入到门户 init
标记中。
state
标记创建用于引用具有特定状态 Portlet 的 URL 字符串。门户框架可以使用返回的 URL 为 Portlet 提供控制链接。每个 URL 都可以通过定义另一个 Portlet 模式或窗口状态来引用新 Portlet 状态。
<state url="<portlet URL>" windowId="<portlet window identifier>" action="<whether this is an action URL: true|false>" portletMode="<portlet mode>" portletWindowState="<window state" var="<variable name for this portlet URL>" scope="<scope for var: page|request|session|application>"> <urlParam name="<parameter name>" value="<parameter value>"/> </state> |
insert
标记调用 Portlet 并检索 Portlet 状态的 Portlet 内容。您可以使用此标记检索 Portlet 可在呈现期间动态指定的 Portlet 标题。
<insert url="<portlet URL>" windowId="<portlet window identifier>" contentVar="<variable name for this portlet content>" contentScope="<scope for contentVar: page|request|session|application>"> titleVar="<variable name for the dynamic portlet title"/> titleScope="<scope for titleVar: page|request|session|application>"> |
所有 Portlet 标记都提供了一个 portlet URL
属性,用于指定 Portlet 上下文和 Portlet 名称。可以使用其他属性来指定更多的细节,如窗口标识符等。
某些 Portlet 标记的属性允许将结果写入变量,而不直接写入到输出流;写入输出流会导致存入缓冲区,因为内容将临时存储在字符串中。由于存入缓冲区会对性能造成影响,因此一般不建议使用此功能。
门户框架常常为 Portlet 内容指定变量,以在实际 Portlet 输出上显示动态 Portlet 标题。应该转而使用 Javascript,如下面的示例所示。
示例
让我们通过一个示例门户 JSP 来了解如何使用聚合标记库。如果您尚未获得示例代码,可以下载示例代码,以便在阅读下面的说明的过程中可以参考它。
此示例将两个 Portlet 呈现于窗口框架中,此框架为每个 Portlet 提供了控制链接,用于更改 Portlet 模式。
首先,在 JSP Header 中,引用聚合标记库并定义命名空间前缀。
<%@ taglib uri="http://ibm.com/portlet/aggregation" prefix="portlet" %> |
此声明使得 JSP 能够识别此标记库,这样您就可以使用所有聚合标记。
接下来,使用 init
标记定义聚合框架的门户上下文。在属性 portletURLPrefix
中声明用于访问门户 JSP 的 URL。
<portlet:init portletURLPrefix="/sample/portal/"> |
门户框架通常由 Servlet 组成,其中包含门户 JSP;不过,此示例仅使用了 JSP。因此,在本例中,您必须在 web.xml 中定义 JSP,以获得引用门户 JSP 的有效门户 URL。门户 URL 不应以 .jsp
等作为结尾。
<?xml version="1.0" encoding="UTF-8"?> <web-app id="WebApp" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <display-name>SamplePortal</display-name> … <servlet> <servlet-name>SamplePortal</servlet-name> <jsp-file>portal.jsp</jsp-file> </servlet> <servlet-mapping> <servlet-name>SamplePortal</servlet-name> <url-pattern>/portal/*</url-pattern> </servlet-mapping> </web-app> |
现在,您可以在指定门户上下文的 init
标记内使用若干 Portlet 标记。
可以通过普通 JSP 或 HTML 处理来获得 Portlet 链接和 Portlet 内容,以实现良好的外观设计。
只要在页面上放置了 Portlet 链接,您就可以使用聚合标记库的 state
标记创建相应的 Portlet URL。下面的 JSP 代码片段显示了如何创建指向 WorldClock Portlet 的视图模式的链接。
<a href="<portlet:state url='worldclock/StdWorldClock' windowId='birga' portletMode='view'/>">view</a> |
windowId
提供了一个标识符,用于保持此 Portlet 的唯一性。然后,您可以将相同的 WorldClock Portlet 再次放置到该页上,同时使用另一个 windowId
。上述链接对定义了不同窗口标识符的 Portlet 没有任何效果。每个 Portlet 窗口必须提供自己的链接。
<a href="<portlet:state url='worldclock/StdWorldClock' windowId='stephan' portletMode='view'/>">view</a> |
使用窗口框架和每个 Portlet 的 Portlet 控制链接设计了页面布局后,通过使用引用 Portlet URL 和窗口标识符的 insert
标记将 Portlet 内容插入到页面中。
<portlet:insert url="worldclock/StdWorldClock" windowId="birga" titleVar="portlettitle_1"/> |
通过为 Portlet 设置的动态标题定义变量,可以使用 Javascript 来将标题嵌入到窗口框架中。
清单 1 显示了已完成的 portal.jsp 的情况。
清单 1. 使用聚合标记库的 portal.jsp
<%@ taglib uri="http://ibm.com/portlet/aggregation" prefix="portlet" %> <%@ page isELIgnored ="false"%> <portlet:init portletURLPrefix="/sample/portal/"> <!-- create portal table of two columns --> <TABLE BORDER="5" WIDTH="100%"> <TR BGCOLOR="BBBBFF"> <TD> <!-- insert portlet title bar into left column --> <TABLE WIDTH="100%"> <TR> <TD> <B><span id="title_1">Portlet 1</span></B> </TD> <TD ALIGN="right"> <a href="<portlet:state url='worldclock/StdWorldClock' windowId='birga' portletMode='view'/>">view</a> <a href="<portlet:state url='worldclock/StdWorldClock' windowId='birga' portletMode='edit'/>">edit</a> <a href="<portlet:state url='worldclock/StdWorldClock' windowId='birga' portletMode='help'/>">help</a> </TD> </TR> </TABLE> </TD> <TD> <!-- insert portlet title bar into right column --> <TABLE WIDTH="100%"> <TR> <TD> <B><span id="title_2">Portlet 2</span></B> </TD> <TD ALIGN="right"> <a href="<portlet:state url='worldclock/StdWorldClock' windowId='stephan' portletMode='view'/>">view</a> <a href="<portlet:state url='worldclock/StdWorldClock' windowId='stephan' portletMode='edit'/>">edit</a> <a href="<portlet:state url='worldclock/StdWorldClock' windowId='stephan' portletMode='help'/>">help</a> </TD> </TR> </TABLE> </TD> </TR> <TR> <TD> <!-- insert portlet into left column --> <portlet:insert url="worldclock/StdWorldClock" windowId="birga" titleVar="portlettitle_1"/> </TD> <TD> <!-- insert portlet into right column --> <portlet:insert url="worldclock/StdWorldClock" windowId="stephan" titleVar="portlettitle_2"/> </TD> </TR> </TABLE> <!-- insert portlet title --> <script type="text/javascript"> document.getElementById("title_1").firstChild.nodeValue = "${portlettitle_1}"; document.getElementById("title_2").firstChild.nodeValue = "${portlettitle_2}"; </script> </portlet:init> |
要查看示例门户页(如图 1 中所示),请执行以下操作:
http://localhost:9080/sample/portal
图 1. 门户聚合 JSP 的示例输出
与 WebSphere Portal 的比较
WebSphere Portal 提供了成熟的聚合框架,您可以使用自己的主题和皮肤对其进行自定义。例如,在 WebSphere Portal 中,您可以在聚合框架内应用细粒度访问控制。
WebSphere Portal 中没有与 WebSphere Application Server V6.1 Portlet 容器提供的聚合标记库对应的功能。聚合标记库允许创建很简单的门户框架来在一个页面上聚合多个 Portlet;它旨在供 Portlet 开发系统使用。
|
访问 Portlet 信息
围绕 Portlet 内容创建窗口框架的门户框架常常提供额外的图标来支持更改 Portlet 模式或窗口状态。为了显示正确的图标,门户框架必须知道 Portlet 支持哪些模式或状态。例如,在显示具有指向 Portlet 的编辑 模式的链接的图标前,门户需要知道 Portlet 是否支持编辑 模式。每个 Portlet 应用程序都会通过 Portlet 部署描述符提供此类信息。MBean 可以访问此类信息。
通常,WebSphere Application Server 的 AdminClient 能够访问所有 MBean。有关如何检索 AdminClient 以及如何使用 MBean 的一般信息,请参见 WebSphere Application 信息中心(在参考资料中列出)的 Developing an administrative client program 部分。
启动 Portlet 应用程序后,将创建以下 Portlet 特定的 MBean:
PortletApplication
类型的 MBean。
其名称由 Portlet 应用程序文件名和后缀 _portletapplication 组成。例如:
StdWorldClock.war_portletapplication
Portlet
类型的 MBean。
其名称由上述 PortletApplication MBean 名称和作为后缀的 Portlet 名称组成。例如:
StdWorldClock.war_portletapplication.StdWorldClock
有关描述 Portlet
和 PortletApplication MBeans
的 Javadoc 的信息,请参见 WebSphere Application Server 信息中心(在参考资料中列出)的 Mbean interfaces 部分。
可以将 MBean API 用于多种用途。此类示例有:
希望确定为 Portlet 提供哪些控制图标或链接的门户框架可能针对特定内容类型检索受支持的 Portlet 模式列表。请参见以下代码片段,以了解如何为 StdWorldClock Portlet 和内容类型 text/html
完成此任务:
ObjectName on = new ObjectName("WebSphere:name=StdWorldClock.war_portletapplication.StdWorldClock,*"); on = (ObjectName) adminService.queryNames(on, null).iterator().next(); HashMap contentTypes = (HashMap) adminService.getAttribute(on, "contentTypes"); List list = (List) contentTypes.get("text/html"); |
除了 Portlet 部署描述符提供的所有信息外,PortletApplication MBean 还定义通知常量,门户可以通过注册 NotificationListener 对此进行侦听。有关如何注册和处理通知事件的信息,请参见 WebSphere Application 信息中心的(在参考资料中列出)Developing an administrative client program 部分。
示例
此示例说明了可以使用 Portlet 特定的 MBean 的一个场景。它将收集关于所有已部署的 Portlet 的信息,以在门户聚合上提供已部署的 Portlet 列表。用户可以从此列表中选择 Portlet,所选的 Portlet 随后显示在聚合中。
要获得对系统中所有 MBean 的访问,请检索 AdminClient:
Properties p = new Properties(); p.put("port", "8880"); p.put("host", "localhost"); p.put("type", "SOAP"); AdminClient adminService = AdminClientFactory.createAdminClient(p); |
所有 Portlet 应用程序都具有 PortletApplication
类型的注册 MBean。因此,可以创建一个 ObjectName 模式来指定 MBean 类型,从而返回系统中所有可用 PortletApplication
Mbean 列表。
ObjectName paons = new ObjectName("WebSphere:type=PortletApplication,*"); Iterator iter = adminService.queryNames(paons, null) .iterator(); while (iter.hasNext()) { ObjectName paon = (ObjectName) iter.next(); … } |
每个 Portlet 应用程序都提供了使用 MBean 属性 portletMBeanNames
访问其 Portlet 列表的功能。要检索对 MBeans 的访问,请创建一个指定 Portlet
MBean 名称的 ObjectName 模式。这样可返回所有使用该名称的可用 Mbean 列表。(尽管返回的是列表,但此列表永远不会包含多个条目。)
Iterator innerIter = ((List) adminService.getAttribute(paon, "portletMBeanNames")).iterator(); while (innerIter.hasNext()) { String name = (String)innerIter.next(); ObjectName pons = new ObjectName("WebSphere:name=" + name + ",*"); ObjectName pon = (ObjectName) adminService.queryNames(pons, null).iterator().next(); … } |
既然您已经能够访问系统中所有的 Portlet 应用程序和 Portlet,就可以查找所需的信息来提供下拉列表,以便从中选择 Portlet。此列表中包含每个可用 Portlet 的 Portlet URL。
当门户使用聚合标记库来显示 Portlet 时,它需要知道对应的 Portlet URL。此 URL 由 Web 应用程序上下文和 Portlet 名称组成,例如 worldclock/StdWorldClock
。
一个应用程序内的所有 Portlet 的 Web 应用程序上下文都是相同的,因此可以将其作为 PortletApplication
MBean 的一个属性进行检索。
String context = (String) adminService.getAttribute(paon, "webApplicationContextRoot"); |
对于 Portlet 名称,请访问 Portlet
MBean 的属性 name
。
String portletName = (String) adminService.getAttribute(on, "name"); |
最后,需要调整门户聚合来提供所有 Portlet URL 的列表,以便用户选择 Portlet。portal.jsp 将随后使用此选择在其聚合中调用该 Portlet。
清单 2 显示了用于将使用 MBean 收集的所有 Portlet url 存储到 Bean 中的代码。
清单 2. 将 Portlet URL 存储到列表中
PortletListBean bean = new PortletListBean(); // add all available portlet URLs to PortletListBean try { Iterator iter = adminService.queryNames(paons, null).iterator(); while (iter.hasNext()) { ObjectName paon = (ObjectName) iter.next(); String context = (String) adminService.getAttribute(paon, "webApplicationContextRoot"); Iterator innerIter = ((List) adminService.getAttribute(paon, "portletMBeanNames")).iterator(); while (innerIter.hasNext()) { String name = (String)innerIter.next(); ObjectName on = new ObjectName("WebSphere:name=" + name + ",*"); on = (ObjectName) adminService.queryNames(on, null).iterator().next(); String portletName = (String) adminService.getAttribute(on, "name"); bean.addURL(context + "/" + portletName); } } } catch (Exception e) { … } |
以此 Bean 为基础,我们将创建一个所有可用 Portlet 的选择列表。更新了 portal.jsp 后,可以通过以下网址安装和访问门户聚合:http://localhost:9080/sample/portal
图 2. 提供所有可用 Portlet 选择的门户聚合
与 WebSphere Portal 的比较
用于访问 Portlet 信息的 MBean API 是 WebSphere Application Server 独有的。在 WebSphere Portal 中,尚未公开任何对应的 Portlet 模型访问机制。
检索系统中可用 Portlet 的信息的需求主要源自门户框架。WebSphere Portal 提供了自己的 Portlet 部署和对象模型来保存和访问有关已安装的 Portlet 的所有信息。因此,WebSphere Portal 并没有必要根据此需求提供公共 API。
|
筛选 Portlet 以修改输出
当通过 URL 直接访问 Portlet 时,可以使用嵌入到筛选器链中名为 PortletDocumentFilters
的筛选器对 Portlet 输出进行修改。预定义的缺省筛选器会将 Portlet 的 Portlet 片段输出转换为有效的文档。这样,Portlet 就可以像 Servlet 一样作为完整文档显示在浏览器中。
所提供的即时可用的 DefaultFilter
能将 Portlet 内容转换为简单的有效 HTML 文档。您可以使用自定义 PortletDocumentFilters
对其进行替换或扩展。
PortletDocumentFilters
为普通 Servlet 筛选器,实现了 javax.servlet.Filter
并遵循 Servlet 筛选器链机制。这里的差异在于声明和进行部署的方式不同。
不应采用直接在 web.xml 中指定筛选器的方式,而应在 plugin.xml 中的扩展点注册 PortletDocumentFilters
(plugin.xml 位于包含 PortletDocumentFilter
的 JAR 文件的根目录中)。
<extension point= "com.ibm.ws.portletcontainer.portlet-document-filter-config"> <portlet-document-filter class-name="sample.WindowFrameFilter" order="1200" /> </extension> |
可以采用两种不同的方式部署 PortletDocumentFilter
:
lib
目录。此 JAR 文件的结构如下:
普通的 Servlet 筛选器概念并不能为希望创建 PortletDocumentFilter 的开发人员提供任何 Portlet 支持。因此,为 PortletDocumentFilters
提供了两个 Helper 来实现 Portlet 特定的需求:
com.ibm.wsspi.portletcontainer.util.FilterRequestHelper
com.ibm.wsspi.portletcontainer.util.PortletURLHelper
FilterRequestHelper
提供了用于控制筛选器流的简单访问。在后台,将由请求属性传输流信息。例如,isDocument
方法将验证之前是否已由某个筛选器将片段转换为文档了。因此,每个筛选器都可以使用 setRedirect
和 setDocument
方法指定特殊需求。
重要:系统并不使用这些属性。每个筛选器必须在转换标记前检查这些属性,以防止筛选器将已经转换的文档再次转换为文档。
PortletURLHelper
将分析 Servlet 请求 URL,以根据 URL Addressability 模式标识 Portlet 信息。这样,Helper 就允许筛选器方便地确定是否请求了 Portlet 操作 URL 或调用的是哪个 Portlet 模式。
为了支持 WML 等其他标记,需要创建新的 PortletDocumentFilter,对其进行注册,然后在缺省 HTML 筛选器之前调用它。新筛选器必须通过对 FilterRequestHelper 调用 setDocument
方法来标记文档转换。此标记可防止 DefaultFilter 将片段转换为 HTML 文档。这样,您就可以使用 PortletDocumentFilters
添加标记支持或修改文档输出。
示例
门户通常会将 Portlet 内容呈现到窗口框架中,并同时提供到不同 Portlet 模式和窗口状态的链接。使用 URL Addressability 时,不会在 Portlet 内容周围显示窗口框架。在此示例中,您将了解如何使用自定义 PortletDocumentFilter 添加简单的窗口框架。
javax.servlet.Filter
),并将其命名为 WindowFrameFilter
。主要使用的方法是 doFilter
方法。
public class WindowFrameFilter implements Filter { public void init(FilterConfig config) throws ServletException {} public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { … } public void destroy() {} } |
doFilter
中,分析 URL 以确定请求是否满足添加窗口框架的对应先决条件:
// do nothing if document cannot be found if (!FilterRequestHelper.isDocument(request)) { chain.doFilter(request, response); return; } |
// analyze request url HttpServletRequest servletRequest = (HttpServletRequest) request; HttpServletResponse servletResponse = (HttpServletResponse) response; String portleturl = null; try { PortletURLHelper urlHelper = new PortletURLHelper("","",servletRequest.getPathInfo()); // do nothing if action called if (urlHelper.isAction()) { chain.doFilter(request, response); return; } portleturl = createPortletURL(servletRequest, urlHelper); } catch (InvalidURLException e) { servletResponse.sendError( e.getStatusCode(), e.getMessage()); return; } |
PrintWriter writer = response.getWriter(); writer.println("<TABLE BORDER='5'>"); writer.println("<TR BGCOLOR='BBBBFF'><TD>"); writer.println("<TABLE WIDTH='100%'>"); writer.println("<TR><TD>"); writer.println("<B><span id='portlet_title_1'>Portlet</span></B>"); writer.println("</TD>"); writer.println("<TD ALIGN='right'>"); writer.println("<a href='" + portleturl + "/state=normal'>normal</a>"); writer.println("<a href='" + portleturl + "/state=maximized'>maximized</a>"); writer.println("<a href='" + portleturl + "/state=minimized'>minimized</a>"); writer.println("</TD></TR>"); writer.println("</TABLE>"); writer.println("</TD></TR><TR><TD>"); chain.doFilter(request, response); writer.println("</TD></TR></TABLE>"); |
String
(根据 URL Addressability 模式引用包含所有所需信息的 Portlet),从而仅忽略变化的窗口状态信息。
StringBuffer buffer = new StringBuffer(); buffer.append(servletRequest.getScheme()); buffer.append("://"); buffer.append(servletRequest.getServerName()); buffer.append(":"); buffer.append(servletRequest.getServerPort()); buffer.append(servletRequest.getContextPath()); buffer.append(servletRequest.getServletPath()); buffer.append("/"); buffer.append(urlHelper.getPortletWindowId()); buffer.append("/ver="); buffer.append(urlHelper.getVersion()); String portleturl = buffer.toString(); |
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.0"?> <plugin id="portletwindowfilter" name="WS_Server" provider-name="IBM" version="1.0.0"> <extension point="com.ibm.ws.portletcontainer.portlet-document-filter-config"> <portlet-document-filter class-name="sample.WindowFrameFilter" order="1200" /> </extension> </plugin> |
lib
目录,并重新启动服务器。
http://localhost:9080/worldclock/StdWorldClock
可以在图 3 中看到其结果。
与 WebSphere Portal 的比较
可以通过 WebSphere Application Server 的 PortletDocumentFilters
对 Portlet 片段进行修改。WebSphere Portal 提供的对应功能是 PortletFilter
概念。
WebSphere Portal 为其 PortletFilter
机制提供了自己的筛选器机制;PortletDocumentFilter
基于 Servlet 筛选器。PortletFilter
嵌入到专用筛选器链中,且要实现 PortletFilter
接口,以便能方便地满足 Portlet 特定的需求。与 PortletDocumentFilter
相比,这些筛选器更灵活;它们并不需要重新启动服务器,因此可以针对每个 Portlet 动态地启用。
|
结束语
在本系列文章的这一部分中,我们更为深入地对 WebSphere Application Server 中的 Portlet 容器进行了分析。我们向您介绍了用于帮助创建简单门户框架的聚合功能。然后,我们说明了如何在通过 URL 寻址时修改 Portlet 输出,以及如何定位和访问 Portlet 信息。另外,我们还说明了这些功能与 WebSphere Portal 中的相应功能的比较情况,以及在寻址 Portlet 方面的差异。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者