Solving Performance Issue in Liferay 6.1 Staging

General Blogs May 30, 2013 By Kan Zhang

Issue

In Liferay 6.1, the staging site is extremely slow if versioning is enabled. It can take 30 seconds to 10 minutes to refresh a page on the staging site (depends on how many contents/portlets on your page).

Cause

If you enable hibernate show SQL by setting the log level to DEBUG for "org.hibernate.SQL" category in "Server Administration" or use JAVA profiler, you will find the following SQL is executed hundreds of times which slows down the staging site a lot:

The AOP class com.liferay.portal.service.impl.LayoutSetLocalServiceStagingAdvice has a pointcut on LayoutSetLocalService. The LayoutSetLocalService can be called hundreds of times in one page refresh. Each time the LayoutSetLocalService is getting called, the LayoutSetLocalServiceStagingAdvice.wrapLayoutSet() will be invoked.

If we look at the code: com.liferay.portal.service.impl.LayoutSetLocalServiceStagingAdvice.wrapLayoutSet()

It will first calls LayoutSetStagingHandler layoutSetStagingHandler = LayoutStagingUtil.getLayoutSetStagingHandler(layoutSet);

Check the code com.liferay.portal.staging.LayoutStagingImpl.getLayoutSetStagingHandler(LayoutSet layoutSet):

The above method will always return null because layoutSet is not a proxy class.

It will return null back to the LayoutSetLocalServiceStagingAdvice.wrapLayoutSet() method. Then the (LayoutSet)ProxyUtil.newProxyInstance( PACLClassLoaderUtil.getPortalClassLoader(), new Class[] {LayoutSet.class}, new LayoutSetStagingHandler(layoutSet)) will be to create a new proxy class to warp the LayoutSet.

Here comes important part: When creating a new LayoutSetStagingHandler(layoutSet), it will call the com.liferay.portal.service.persistence.LayoutSetBranchFinderImpl.findByMaster(long groupId, boolean privateLayout) method. But the LayoutSetBranchFinderImpl finder method does not have the Liferay finder cache enabled, so Liferay will query the database every time it is getting called.

So, each time LayoutSetLocalService is getting called, there will be at least one database query to the LayoutSetBranch table, which makes the staging site very slow.

Solution

The best solution in my mind is to change the way how Liferay handles the branching/versioning in Liferay staging or to reduce the call to LayoutSetLocalService class. But it requires a lot of work. I am still working on it and will send a pull request if I can finish it.

Another solution is to use ext plugin to overwrite the LayoutSetBranchFinderImpl class to add the Liferay finder cache and overwrite LayoutSetBranchPersistenceImpl class to clear the Liferay finder cache upon create/update/delete.

If you do not want to use ext environment, here is a workaround using hook plugin:

Override the com.liferay.portal.service.LayoutSetBranchLocalService in the hook plugin:

Implement the com.kzhang.liferay.portal.service.LayoutSetBranchLocalServiceWrapper:

The main idea is to leverage getLayoutSetBranches(groupId, privateLayout) method to get the master LayoutSetBranch instead of using the finder method, because the getLayoutSetBranches() method has Liferay entity cache enabled. But as you can see, it has a silly for loop in the code which is not cool.

After deployed the hook, the page loading time is reduced from 3 minutes to 2 seconds.

Or, what you can do is: you can use getLayoutSetBranch(groupId, privateLayout, "main-variation") instead of getLayoutSetBranches(groupId, privateLayout) to get better performance if you never change the name of the master branch (the default name of the master branch is "main-variation").

RE:5 tips to improve usage of the Liferay script console

General Blogs May 13, 2013 By Kan Zhang

// In reply to 5 tips to improve usage of the Liferay script console - by Sébastien Le Marchand

Sébastien wrote a very good article about Liferay script console. Here is one more tip in response of the call for sharing at the end of the article. I post it as a sepreate blog entry because I cannot use Gist or format in comment area....

 

Tip #6: Schedule your script to run as a CRON job

If you want to schedule your script to execute at certain times as a CRON job (One scenario can be: Run the task "deactivate users never logged and created since more than 2 years" every month.), you can use Liferay's Scheduler Engine API, like in the following example:
The following code outputs the string "test" to a file one time per minute in the next 10 minutes:
 
You can change your job schedule by changing the parameters in CronText constructor.
 
You will also need to use proper escape characters in the script.
 
Note that when Liferay Scheduler Engine execute the script, the following predefined variables are not available:
  • out (java.io.PrintWriter)
  • actionRequest (javax.portlet.ActionRequest)
  • actionResponse (javax.portlet.ActionResponse)
  • portletConfig (javax.portlet.PortletConfig)
  • portletContext (javax.portlet.PortletContext)
  • preferences (javax.portlet.PortletPreferences)
  • userInfo (java.util.Map<String, String>)
    

 

Using custom java classes in a JSP hook

General Blogs May 10, 2013 By Kan Zhang

Issue

In Liferay hook development, one common question is:

How to import/use/access custom java classes in a JSP hook?

The question can be described as:

1. Create a hook project (for example: sample-hook)

2. Create a custom class (for example: com.kzhang.CustomJavaClass) in hook project.

3. Override a Liferay portlet jsp (for example, html/portlet/blogs/view.jsp) and try to use the CustomJavaClass in the jsp. Liferay will throw "class CustomJavaClass can't be resolved to a type" exception or ClassNotFound exception.

 

Cause

The jsps in hook plugin is deployed to ROOT servlet context. for example, the html/portlet/blogs/view.jsp in hook plugin will be copied to ${container.deploy.folder}/ROOT/html/portlet/blogs/view.jsp, and the origional view.jsp will be renamed as view.portal.jsp.

The custom classes are still remains in the hook servlet context. for example, the com.kzhang.CustomJavaClass in hook plugin will be still in ${container.deploy.folder}/sample-hook/WEB-INF/classes/com/kzhang/CustomJavaClass.class.

In html/portlet/blogs/view.jsp it can not find CustomJavaClass because each servlet context can only see it's own classes and those of the parent classloaders. In our case, the html/portlet/blogs/view.jsp is in ROOT servlet context which can not see the CustomJavaClass in sample-hook servlet context

 

Solution

There are some discussions in Liferay forum:
http://www.liferay.com/community/forums/-/message_boards/message/7454307
http://www.liferay.com/community/forums/-/message_boards/message/13467179
http://www.liferay.com/community/forums/-/message_boards/message/21397246
http://www.liferay.com/community/forums/-/message_boards/message/11508053

The most common solution is to use the ext plugin instead of hook plugin.

But can we avoid using the ext plugin and leave everything as a hook?

Solution 1: Use Spring + PortletBeanLocatorUtil + JAVA reflection

Step 1. Create the custom-spring.xml in /sample-hook/docroot/WEB-INF/src/context/custom-spring.xml folder.

Step 2. In custom-spring.xml, define the bean for your custom java class:

Step 3. In web.xml, add the context-param to load the custom-spring.xml:

Step 4. In custom_jsps/html/portlet/blogs/view.jsp, use PortletBeanLocatorUtil to load the custom java class:

If your CustomJavaClass implements an interface which is accessable in ROOT servlet context, then you can cast the customJavaClass to the interface and use the class thru interface. Otherwise you will need to use JAVA reflection to dynamically invoke the methods in the custom class.

Example here.

Solution 2: Override Struts Action and call the custom classes in Struts Action.

Setp 1. In liferay-hook.xml, override the view action of blogs portlet:

Step 2. Create the CustomStrutsAction, invoke the custom class in CustomStrutsAction and set the result in to renderRequest attribute:

( Instead of result, you can also set the custom class in the renderRequest attribute and invoke it in jsp either by using it's Interface or use java reflection)

Step 3. In custom_jsps/html/portlet/blogs/view.jsp, get the result from the renderRequest attribute:

Using Gist in Liferay Community Blogs

General Blogs April 24, 2013 By Kan Zhang

All credit goes to James Falknersmiley

Steps given by James:

Note 1: The Gist is not viewable in "Preview".
Note 2: The CSS on Liferay site ate all the indents in Gist because of the following in Liferay CSS:

To fix the indents, simply insert the following CSS in your blog entry in "Source" view:

Performance bottleneck in JSON web services in Liferay 6.0.x

General Blogs April 23, 2013 By Kan Zhang

This issue is addressed in Liferay 6.1.x. It only affacts Liferay 6.0.x.

Issue:

There is one performance bottleneck in JSON web services in Liferay 6.0.x: JSONServiceAction class is loading service class per request.

When doing a load test on the Liferay JSON web services (Example below), the performance is bad.

http://<IP_ADDRESS>:8080/tunnel-web/secure/json?serviceClassName=com.liferay.portal.service.CountryServiceUtil&serviceMethodName=getCountries

Cause:

In JSONServiceAction.getJSON() method:

On each of the request, the above code will load the service class based on the serviceClassName request parameter.

But the classloader.loadClass is a synchronized method, it becomes a performance bottleneck.

Solution:

The project I was working on about two years ago only requires to access a certain set of JSON services. My strategy was to preload the service classes into a cache to avoid runtime class loading.

In Liferay ext plugin:

1. Create ServiceCacheUtil class:

2. create CustomJSONServlet. This allows me to configure the list of service classes to preload in tunnel-web's web.xml and to use the CustomJSONServiceAction (see step 4) class.

3. Customize web.xml in tunnel-web to use the CustomJSONServlet:

4. create a CustomJSONServiceAction which extends JSONServiceAction:

5. You will need to do the same steps above in your custom portlet if you have a Service Builder generated custom JSON service in your portlet.

Note: The issue already got resolved in Liferay 6.1.x using @JSONWebService anotation. The @JSONWebService anotated services will be registered in the JSONWebServiceActionsManager to avoid runtime classloading.

Warning: The above code is not tested. I have implemented it two years ago but unfortunately I do not have that code any more. The above code I wrote is based on my memory - Use at your own risk.

 

Using legacy DAOs with Liferay

General Blogs April 18, 2013 By Kan Zhang

Sten Martinez wrote a very nice article about Using a Legacy DB with Liferay Service Builder:

http://www.liferay.com/web/sten.martinez/blog/-/blogs/using-a-legacy-db-with-service-builder

It inspires me to start thinking: What if I want to use a Legacy DB WITHOUT Liferay Service Builder?

A lot of time when we have a legacy DB, most likely we will also have a legacy Spring/Hibernate based J2EE application. In the legacy Spring/Hibernate based J2EE application, you have all the DAOs implemented. How do I leverage these legacy DAOs in Liferay?

 

Prerequisites

Export the DAO classes in to a jar file and put them in container’s global lib folder.

 

1.Using legacy DAOs in a Spring managed class.

There are a many Class is managed by Spring. Such as the classes in a Spring MVC portlet, the Service Builder generated classes and most of the Liferay classes.

Using the legacy DAOs in a Spring managed class is straightforward. We can add the bean definitions for the legacy DAOs in the Spring context file and wire them with other Spring managed classes.

 

2.Using legacy DAOs in a Non-Spring managed class.

Some of the Classes are not managed by Spring in Liferay. Such as Liferay MVC portlet, ServicePreAction/ServicePostAction, LoginPreAction/LoginPostAction etc. We cannot directly inject the DAO dependencies into these classes.

We need to com.liferay.util.bean.PortletBeanLocatorUtil to load the legacy DAOs:

  1. Add the spring context file in web.xml:

<context-param>

                <param-name>portalContextConfigLocation</param-name>

                <param-value>/WEB-INF/classes/META-INF/dao-spring.xml</param-value>

</context-param>

  1. In the “dao-spring.xml” we can define the session factory and DAO beans and wire them with Controller/Service classes. Just like a normal Spring application.
  2. Use the com.liferay.util.bean.PortletBeanLocatorUtil to load the DAOs. For example, In the Liferay MVC portlet, if you configured <portlet-class>com.sample.mvc.SampleMVCPortlet</portlet-class> in portlet.xml and want to use legacy DAOs in the com.sample.mvc.SampleMVCPortlet:

public class SampleMVCPortlet extends MVCPortlet {

                private static SomeDAO someDAO;

                @Override

                public void doView(RenderRequest renderRequest,

                                                RenderResponse renderResponse) throws IOException, PortletException {

                               

                                SomeObject someObject = getSomeDAO().getSomeObject();

 

                                super.doView(renderRequest, renderResponse);

                }

    private SomeDAO getSomeDAO() {

        if (someDAO == null) {

                someDAO = (SomeDAO) PortletBeanLocatorUtil

                    .locate(SomeDAO.class.getName());

        }

        return someDAO;

    }

}

We can use the same approach in ServicePreAction, LoginPostAction etc.

Resolve the "Sass::SyntaxError: Invalid CSS" error in Liferay 6.1

General Blogs April 17, 2013 By Kan Zhang

Issue:

Liferay 6.1 uses the dynamic CSS filter to parse Sass CSS. The dynamic CSS filter will validate the SASS/CSS and compile the SASS into regular CSS and put them into .sass-cache folder.

But in Liferay 6.1, The dynamic CSS filter always give "Sass::SyntaxError: Invalid CSS" exception on many VAILD CSS files.  For example, If you want to use the twitter bootstrap CSS in to your custom theme, you will get the following ERROR:

ERROR [MinifierFilter:421] Unable to parse SASS on CSS /opt/liferay/liferay-portal-6.1.0-ce-ga1/tomcat-7.0.23/${YOUR_CUSTOM_THEME}/css/bootstrap.css
org.jruby.embed.EvalFailedException: (SyntaxError) Expected a color. Got: transparent

Cause:

The issue is caused by the old campass library (compass-0.11.5) which has an issue in handling the transparent keyword: https://github.com/chriseppstein/compass/pull/715#issuecomment-4186662

It got fixed in the latest campass library.

Solution:

We need to upgrade the campass library to the newer version. You can find the newer version of ruby-gems.jar (which contains the campass library) in Liferay 6.2 M4 under liferay-portal-6.2.0-ce-m4/tomcat-7.0.34/webapps/ROOT/WEB-INF/lib folder.

Copy the ruby-gems.jar from Liferay 6.2 M4 then put it to webapps/ROOT/WEB-INF/lib of your Liferay 6.1 server.

 Add the following to your portal-ext.properties (Note: Don't miss the backslash in the end of the first line):

scripting.jruby.load.paths=\
classpath:/META-INF/jruby.home/lib/ruby/site_ruby/1.8,\
classpath:/META-INF/jruby.home/lib/ruby/site_ruby/shared,\
classpath:/gems/chunky_png-1.2.6/lib,\
classpath:/gems/compass-0.12.2/lib,\
classpath:/gems/fssm-0.2.9/lib,\
classpath:/gems/sass-3.2.1/lib,\
${java.io.tmpdir}/liferay/ruby/gems/chunky_png-1.2.6/lib,\
${java.io.tmpdir}/liferay/ruby/gems/compass-0.12.2/lib,\
${java.io.tmpdir}/liferay/ruby/gems/fssm-0.2.9/lib,\
${java.io.tmpdir}/liferay/ruby/gems/sass-3.2.1/lib

Delete the "ruby" folder under your {java.io.tmpdir}/liferay folder to ensure Liferay to use the newest campass library.

Note:

You may wondering why we are using ${java.io.tmpdir} here. This is because JRuby has some problem loading the files inside a jar file. To overcome the problem, Liferay implemented a workaround: extract the ruby-gems.jar to ${java.io.tmpdir}/liferay/ruby/ folder and tell the JRuby to load the files there as a backup plan. You can find the logic in com.liferay.portal.scripting.ruby.RubyExecutor.initRubyGems().

 

Showing 7 results.
Items 20
of 1