Liferay最大的好处是不仅接口强大,利于扩展,就连JSP定制扩展都提供了3种方式。
修改核心jsp代码,有3种修改方式:
1、暴力修改
直接修改(位于portal-web/docroot/html),编译部署,会带来风险,而且不能同步更新。
2、全量扩展修改
热部署jsp文件(替代原有jsp),这是v7.0下的OSGi方式,实现方式非常优雅。
3、CustomJspBag Hook方式
实现CustomJspBag接口,做jsp片段式的修改,同样是增量热部署,也是v7.0下的OSGi方式(需要增加依赖org.osgi.service.component.annotations.Activate和org.osgi.service.component.annotations.Component;),实现方式可以说更加优雅。
第一种方式不讲了。
2、全量扩展修改
需要新建fragment module。只需要注意2点:
1、在module工程的OSGi定义(bnd.bnd文件)中指定Fragment-Host声明
如下:
Bundle-Version: 1.0.0
Fragment-Host: com.liferay.login.web;bundle-version="1.0.0"
-sources: true
2、把你修改后的JSP文件放在module工程的resources目录,比如
你的module工程srcmain
esourcesMETA-INF
esourceslogin.jsp
login.jsp例子:
<%-- /** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ --%> <%@ include file="/init.jsp" %> <p style="color: red">changed</p> <c:choose> <c:when test="<%= themeDisplay.isSignedIn() %>"> <% String signedInAs = HtmlUtil.escape(user.getFullName()); if (themeDisplay.isShowMyAccountIcon() && (themeDisplay.getURLMyAccount() != null)) { String myAccountURL = String.valueOf(themeDisplay.getURLMyAccount()); signedInAs = "<a class="signed-in" href="" + HtmlUtil.escape(myAccountURL) + "">" + signedInAs + "</a>"; } %> <liferay-ui:message arguments="<%= signedInAs %>" key="you-are-signed-in-as-x" translateArguments="<%= false %>" /> </c:when> <c:otherwise> <% String redirect = ParamUtil.getString(request, "redirect"); String login = LoginUtil.getLogin(request, "login", company); String password = StringPool.BLANK; boolean rememberMe = ParamUtil.getBoolean(request, "rememberMe"); if (Validator.isNull(authType)) { authType = company.getAuthType(); } %> <portlet:actionURL name="/login/login" secure="<%= PropsValues.COMPANY_SECURITY_AUTH_REQUIRES_HTTPS || request.isSecure() %>" var="loginURL" /> <aui:form action="<%= loginURL %>" autocomplete='<%= PropsValues.COMPANY_SECURITY_LOGIN_FORM_AUTOCOMPLETE ? "on" : "off" %>' cssClass="sign-in-form" method="post" name="fm" onSubmit="event.preventDefault();"> <aui:input name="saveLastPath" type="hidden" value="<%= false %>" /> <aui:input name="redirect" type="hidden" value="<%= redirect %>" /> <aui:input name="doActionAfterLogin" type="hidden" value="<%= portletName.equals(PortletKeys.FAST_LOGIN) ? true : false %>" /> <c:choose> <c:when test='<%= SessionMessages.contains(request, "userAdded") %>'> <% String userEmailAddress = (String)SessionMessages.get(request, "userAdded"); String userPassword = (String)SessionMessages.get(request, "userAddedPassword"); %> <div class="alert alert-success"> <c:choose> <c:when test="<%= company.isStrangersVerify() || Validator.isNull(userPassword) %>"> <liferay-ui:message key="thank-you-for-creating-an-account" /> <c:if test="<%= company.isStrangersVerify() %>"> <liferay-ui:message arguments="<%= userEmailAddress %>" key="your-email-verification-code-has-been-sent-to-x" translateArguments="<%= false %>" /> </c:if> </c:when> <c:otherwise> <liferay-ui:message arguments="<%= userPassword %>" key="thank-you-for-creating-an-account.-your-password-is-x" translateArguments="<%= false %>" /> </c:otherwise> </c:choose> <c:if test="<%= PrefsPropsUtil.getBoolean(company.getCompanyId(), PropsKeys.ADMIN_EMAIL_USER_ADDED_ENABLED) %>"> <liferay-ui:message arguments="<%= userEmailAddress %>" key="your-password-has-been-sent-to-x" translateArguments="<%= false %>" /> </c:if> </div> </c:when> <c:when test='<%= SessionMessages.contains(request, "userPending") %>'> <% String userEmailAddress = (String)SessionMessages.get(request, "userPending"); %> <div class="alert alert-success"> <liferay-ui:message arguments="<%= userEmailAddress %>" key="thank-you-for-creating-an-account.-you-will-be-notified-via-email-at-x-when-your-account-has-been-approved" translateArguments="<%= false %>" /> </div> </c:when> </c:choose> <liferay-ui:error exception="<%= AuthException.class %>" message="authentication-failed" /> <liferay-ui:error exception="<%= CompanyMaxUsersException.class %>" message="unable-to-log-in-because-the-maximum-number-of-users-has-been-reached" /> <liferay-ui:error exception="<%= CookieNotSupportedException.class %>" message="authentication-failed-please-enable-browser-cookies" /> <liferay-ui:error exception="<%= NoSuchUserException.class %>" message="authentication-failed" /> <liferay-ui:error exception="<%= PasswordExpiredException.class %>" message="your-password-has-expired" /> <liferay-ui:error exception="<%= UserEmailAddressException.MustNotBeNull.class %>" message="please-enter-an-email-address" /> <liferay-ui:error exception="<%= UserLockoutException.LDAPLockout.class %>" message="this-account-is-locked" /> <liferay-ui:error exception="<%= UserLockoutException.PasswordPolicyLockout.class %>"> <% UserLockoutException.PasswordPolicyLockout ule = (UserLockoutException.PasswordPolicyLockout)errorException; %> <liferay-ui:message arguments="<%= ule.user.getUnlockDate() %>" key="this-account-is-locked-until-x" translateArguments="<%= false %>" /> </liferay-ui:error> <liferay-ui:error exception="<%= UserPasswordException.class %>" message="authentication-failed" /> <liferay-ui:error exception="<%= UserScreenNameException.MustNotBeNull.class %>" message="the-screen-name-cannot-be-blank" /> <aui:fieldset> <% String loginLabel = null; if (authType.equals(CompanyConstants.AUTH_TYPE_EA)) { loginLabel = "email-address"; } else if (authType.equals(CompanyConstants.AUTH_TYPE_SN)) { loginLabel = "screen-name"; } else if (authType.equals(CompanyConstants.AUTH_TYPE_ID)) { loginLabel = "id"; } %> <aui:input autoFocus="<%= windowState.equals(LiferayWindowState.EXCLUSIVE) || windowState.equals(WindowState.MAXIMIZED) %>" cssClass="clearable" label="<%= loginLabel %>" name="login" showRequiredLabel="<%= false %>" type="text" value="<%= login %>"> <aui:validator name="required" /> </aui:input> <aui:input name="password" showRequiredLabel="<%= false %>" type="password" value="<%= password %>"> <aui:validator name="required" /> </aui:input> <span id="<portlet:namespace />passwordCapsLockSpan" style="display: none;"><liferay-ui:message key="caps-lock-is-on" /></span> <c:if test="<%= company.isAutoLogin() && !PropsValues.SESSION_DISABLED %>"> <aui:input checked="<%= rememberMe %>" name="rememberMe" type="checkbox" /> </c:if> </aui:fieldset> <aui:button-row> <aui:button type="submit" value="sign-in" /> </aui:button-row> </aui:form> <liferay-util:include page="/navigation.jsp" servletContext="<%= application %>" /> <aui:script sandbox="<%= true %>"> var form = AUI.$(document.<portlet:namespace />fm); form.on( 'submit', function(event) { var redirect = form.fm('redirect'); if (redirect) { var redirectVal = redirect.val(); redirect.val(redirectVal + window.location.hash); } submitForm(form); } ); form.fm('password').on( 'keypress', function(event) { Liferay.Util.showCapsLock(event, '<portlet:namespace />passwordCapsLockSpan'); } ); </aui:script> </c:otherwise> </c:choose>
3、CustomJspBag Hook方式
这是一种覆盖原有XXXX-ext.jsp的方式,XXXX-ext.jsp是空文件,下面的例子用来覆盖 bottom.jsp ,插入了新定义 bottom-ext.jsp 片段进来
文件位于 src/main/resources/META-INF/jsps/html/common/themes/bottom-ext.jsp
bottom-ext.jsp 只有一行
<h2>HERE I AM!!!!!</h2>
然后实现CustomJspBag接口,实现类YourCustomJspBag ,有2点需要特别注意
service.ranking是优先级的意思,数值越大越优先,在例子中是100,当有另外的CustomJspBag实现类,比如定义为200,那么定义为200的类的优先级更高。
关键方法是activate,作用是为所有的需要自定义的核心JSPs添加URL(目录路径)到List列表,List列表的用途有些过滤器的含义,例子中是添加"META-INF/jsps/"下的所有文件。
import com.liferay.portal.deploy.hot.CustomJspBag; import com.liferay.portal.kernel.url.URLContainer; import java.net.URL; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @Component( immediate = true, property = { "context.id=YourCustomJspBag", "context.name=Custom JSP Bag", "service.ranking:Integer=100" } ) public class YourCustomJspBag implements CustomJspBag { @Override public String getCustomJspDir() { return "META-INF/jsps/"; } /** * 返回自定义JSP URL paths列表. */ @Override public List<String> getCustomJsps() { return _customJsps; } @Override public URLContainer getURLContainer() { return _urlContainer; } @Override public boolean isCustomJspGlobal() { return true; } //osgi激活时触发的动作 @Activate protected void activate(BundleContext bundleContext) { _bundle = bundleContext.getBundle(); _customJsps = new ArrayList<>(); Enumeration<URL> entries = _bundle.findEntries( getCustomJspDir(), "*.jsp", true); while (entries.hasMoreElements()) { URL url = entries.nextElement(); //*.jsp全部添加进JSP URL paths _customJsps.add(url.getPath()); } } private Bundle _bundle; private List<String> _customJsps; private final URLContainer _urlContainer = new URLContainer() { @Override public URL getResource(String name) { return _bundle.getEntry(name); } @Override public Set<String> getResources(String path) { Set<String> paths = new HashSet<>(); for (String entry : _customJsps) { if (entry.startsWith(path)) { paths.add(entry); } } return paths; } }; }
gradle的配置文件build.gradle
dependencies { compile 'com.liferay.portal:com.liferay.portal.kernel:2.0.0' compile 'com.liferay.portal:com.liferay.portal.impl:2.0.0' compile 'org.osgi:org.osgi.core:6.0.0' compile 'org.osgi:org.osgi.service.component.annotations:1.3.0' } version = '1.0.0'
部署后的界面:
CustomJspBag后面的秘密
在portal-web/docroot/html/common/themes/bottom.jsp 文件,在其最下方,有以下代码:
<liferay-util:include page="/html/common/themes/bottom-ext.jsp" />
原来必须要依靠原来的JSP包括了一个空的bottom-ext.jsp文件,这是前提
实际上它只是覆盖了bottom-ext.jsp,而不是它的宿主bottom.jsp
即所有类似XXXX-ext.jsp这样的文件,都是可以做定制的。
那么看到这里就很清晰第三种方式(CustomJspBag Hook)和第二种方式(全量扩展修改)之间的区别了,即 片段覆盖 和 全量覆盖的区别,这需要您根据需求来做选择。
Liferay给开发者这两种选择,目的很清晰,即通过CustomJspBag Hook来降低风险,做合理的分离设计。