- 论坛徽章:
- 0
|
Overview
One of the key strengths of JavaServer Faces (JSF) is that not only does it
provide substantial technology for easy, out of the box component based J2EE
Web applications assembly, but it also is a very flexible API which allows for
a wide breadth of customizations in numerous and innovative ways. This article
introduces and explores the component developer's experience of building custom
JSF user interface (UI) components.
Intended Audience
First off, this article is intended for component developers who already
have an understanding of the overall JSF application development process but
want to learn how to start developing their own custom JSF UI components. For
those who are still new to JSF development, readers are highly recommended to
first consult JSF application tutorials such as Rick Hightower's article on
basic JSF development:
http://www-106.ibm.com/developerworks/library/j-jsf1/
Or my earlier JavaPro article, which also introduces JSF:
http://www.fawcette.com/javapro/2004_01/magazine/features/cschalk/
In contrast to those introductory JSF articles, this article introduces the
more advanced task of building custom JSF UI components by first explaining
all of the "moving parts" associated with custom UI Components starting
with a simple custom "HelloWorld" UI Component as an example.
The article then shows how to extend the simple "HelloWorld" component
into more useful examples that show live stock quote information using a Web
service. At the end of the article readers will have an appreciation of what
exactly are the key tasks associated with building custom UI Components along
with a full understanding of all the various sub-components that make up a typical
"JSF UI Component".
When to Customize JSF
Before jumping into custom JSF component development just for the sake of it,
some research should be done on whether custom development is necessary at all
and at what level it is needed. As mentioned before, JavaServer Faces' base
UI component library actually provides a modest amount of practical UI and non-UI
components in its specification. In addition to visible components such as input/output
fields, menu components and form buttons the JSF specification also has non-UI
components which handle things like basic validation and/or data conversion.
In many cases a developer may find that he/she may only need to customize a
specific sub-component such as a new validation method as opposed to building
an entirely new UI Component.
An easy
google
search or browsing through sites
such as
jsfcentral.com
many usable and often
free component libraries (and implementations) that can be experimented with.
Some of the more popular and powerful ones include:
MyFaces - A general purpose and open source component library
and implementation. -
http://myfaces.apache.org
Oracle's ADF Faces - A rich library of useful JSF UI Components
-
http://www.oracle.com/technology/products/jdev/htdocs/partners/addins/exchange/jsf/index.html
JScape's WebGalileo Faces -
http://www.jscape.com/webgalileofaces/
Once the decision has been made to build some custom components as opposed
to just using some existing ones, here's how to get started.
The "Moving Parts" of a JSF UI Component
The term "JSF UI Component" is generally used to describe a set of
sub-components which each which perform their own specific task such as rendering
the component, validating its input, and/or performing any data conversions
(such as a String to a Date conversion). This may often give the impression
to a novice JSF developer that JSF component development is fairly complicated
and possibly tedious, but this is also JSF's strength in that each UI Components'
sub-components can be individually customized and re-configured into new and
varied combinations. In fact it is JSF's flexible API and it's "pluggable
renderering" technology which allows UI Components to written for multiple
Web clients thus drastically decreasing the complexity for the Web developer
of assembling multi-client Web applications. Let's review the various "moving
parts" of what constitutes a JSF UI Component.
The general term "JSF UI Component" refers to a collection of the
following:
UIComponent Class - A Java class derived from either the UIComponentBase
or extended from an existing JSF UIComponent such as outputText. This is the
actual Java class representing the core logic of the component. It can optionally
contain the logic to "render" itself to a client, or rendering logic
can be separated into a separate "renderer" class. Renderer Class - This is a class that only contains code to render
a UIComponent. Rendering classes can provide different renderings for a UI
Component. For such as either a button or hyperlink for the UICommand component.
Renderers can also provide different rendering options for the same UI Component
on different client types such as browsers, PDAs etc. This allows JSF
the ability to run on any client device providing a corresponding set of renderer
classes are built for the specific client type.UI Component Tag Class - This is a JSP tag handler class that allows
the UI Component to be used in a JSP. It can also associate a separate renderer
class with a UIComponent class. (Note: The UI Component Tag handler class
is only required for usage in a JSP deployment environment. Keep in mind that
UI Components can also be used in non-JSP environments.)Tag Library Descriptor File - This is a standard J2EE JSP tag library
descriptor (tld) file which associates the tag handler class with a usable
tag in a JSP page. (Required only For JSP usage only.)Associated helper classes - These include a collection of standard
(included in JSF RI) or custom helper classes such as Converters, Validators,
ActionListeners etc., that can be programmatically bound to UI Components.
For example a JSF UI Input Field component can be associated with a built-in
number range Validator which ensures that an entered number is within a certain
range. These helper classes can also be customized to perform any type of
validation not provided out of the box with the JSF RI. For example a custom
credit card Validator could be used with a JSF Input field to validate credit
card numbers or a custom Converter could be used to convert currencies.
A Simple "HelloWorld" Custom UI Component Example
Before showing how to build a "Hello World" custom component, let's
review quickly how to use it. To invoke the component we have a JSF enabled
JSP page with an extra taglib directive specifying a custom tag library (tld)
which contains the custom tag, "jsfhello", which is associated
with our custom UI Component. To use the jsfhello tag, we simply place it into
the body of the JSP and set an optional "hellomsg" attribute.
A simple HelloWorld Custom Component:
The HelloWorld UI Component:
When the page is run, the JSF UI Component displays the "Hello World"
message provided in the "hellomsg" attribute along with the current
date and time. We'll review the details of the tag library and how the tag handler
class invokes the UI Component shortly, but first let's review how to build
the core UI Component class.
Building the"HelloWorld" Custom UI ComponentStep 1: Creating a UIComponent Class
For our HelloWorld example, our UIComponent will extend the UIComponentBase
abstract class, which is provided in the JSF specification, and render a formatted
HelloWorld message. Note: We could have also based our new UI Component class
on the concrete UIOutput base class which is basically the same as the UIComponentBase
with the exception that it has a "value" attribute. Since we aren't
using one, we'll simply base it on the UIComponentBase abstract class.
package tss.hello;
import java.util.Date;
import javax.faces.component.UIComponentBase;
import javax.faces.context.FacesContext;
import java.io.IOException;
import javax.faces.context.ResponseWriter;
public class HelloUIComp extends UIComponentBase
{
public void encodeBegin(FacesContext context) throws IOException
{
ResponseWriter writer = context.getResponseWriter();
String hellomsg = (String)getAttributes().get("hellomsg");
writer.startElement("h3", this);
if(hellomsg != null)
writer.writeText(hellomsg, "hellomsg");
else
writer.writeText("Hello from a custom JSF UI Component!", null);
writer.endElement("h3");
writer.startElement("p", this);
writer.writeText(" Today is: " + new Date(), null);
writer.endElement("p");
}
public String getFamily()
{
return "HelloFamily";
}
}
As you can see, this custom UI Component renders a formatted Helloworld message
using and encodeBegin()getFamily(). This is actually required for UI Components that extend
UIComponentBase since it could come from any component family. Since in our
example we won't be creating a new family of components, we can just return
any String value such as "HelloFamily". Before examing the code further,
a quick note on encoding and decoding. method. Also notice that we have defined a method
A Quick Note on Encoding Methods
UI Components and/or associated renderer classes use encode*() methods
display themselves to a client. The above HelloWorld example simply renders
an HTML header (h3) with the custom hello message (if supplied) along with a
paragraph containing the current date and time. For this component a single
encodeBegin() (or encodeEnd() ) method is all that is needed to render the complete
tag since it does not contain any children. The encodeBegin actually renders
the beginning or the complete (bodiless) tag. UI Components with children tags/components
will override encodeChildren() along with encodeEnd(). The encodeChildren()
method allows children components to be rendered and encodeEnd() renders the
closing parent tag.
A Closer Look at the encodeBegin() Method
Upon closer examination of the encodeBegin() method, we see that it receives
the FacesContext as an argument. An extension to the servlet and JSP context,
the FacesContext provides access to the many useful objects for JSF/JSP/Servlet
development. In this example, we simply extract a "writer" object
in order to "write" our rendered response back to the client.
public void encodeBegin(FacesContext context) throws IOException { ResponseWriter
writer = context.getResponseWriter();
Next we get the value of the attribute "hellomsg" which is passed
from the tag from our JSP page using getAttributes(). The getAttributes method
comes from the UIComponentBase class which is the base class for all UI Components.
String hellomsg = (String)getAttributes().get("hellomsg");
Rendering the HTML message is done using the writer methods in the code:
writer.startElement("h3", this);
if(hellomsg != null)
writer.writeText(hellomsg, "hellomsg");
else
writer.writeText("Hello from a custom JSF UI Component!", null);
writer.endElement("h3");
writer.startElement("p", this);
writer.writeText(" Today is: " + new Date(), null);
writer.endElement("p");
which when executed displays a formatted HTML "HelloWorld" message
to the client.
Note: You'll notice that when the attribute (hellomsg) or "property"
is written to the client, an additional String value representing the name of
the property "hellomsg" is also included as an argument to the writeText()
method. The idea behind this is to provide development tools environments the
ability to display the name of the property in a visual editor. Leaving the
second argument null is also acceptable and will not harm the execution
of the component.
Step 2. Registering the custom UI Component in Faces-config.xml
Before moving on to building a JSP tag handler and a TLD file, we'll add a
required entry for our custom component in the faces-config.xml file. The syntax
for adding our custom UI component is:
...
tss.hello.JsfHello
tss.hello.HelloUIComp
...
The component-type is the "JSF recognized" name of our custom component:
"tss.hello.JsfHello". (We'll refer to this later in our tag handler.)
The component-class is the actual class path address of our UI Component.
Step 3. Building a Custom JSP Tag Library
In order to be able to use our custom component in a JSP, we need a custom
tag library comprised of a tag library descriptor file (TLD) along with references
to taghandlers classes. (This example uses just a single tag handler.)
Building the Tag Handler
For JSF component development, the JSP taghandler class is derived from javax.faces.webapp.UIComponentTag.
It's main purpose is to:
Associate a JSP callable tag (handler class) with the UI Component.Associate a separate renderer class (if needed) to the UI Component.Set the properties from the submitted tag attribute values to the UI Component.
Here is the source code for our tag handler class:
package tss.hello;
import javax.faces.application.Application;
import javax.faces.webapp.UIComponentTag;
import javax.faces.component.UIComponent;
import javax.faces.el.ValueBinding;
import javax.faces.context.FacesContext;
public class FacesHelloTag extends UIComponentTag
{
// Declare a bean property for the hellomsg attribute.
public String hellomsg = null;
// Associate the renderer and component type.
public String getComponentType() { return "tss.hello.JsfHello"; }
public String getRendererType() { return null; }
protected void setProperties(UIComponent component)
{
super.setProperties(component);
// set hellomsg
if (hellomsg != null)
{
if (isValueReference(hellomsg))
{
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding(hellomsg);
component.setValueBinding("hellomsg", vb);
}
else
component.getAttributes().put("hellomsg", hellomsg);
}
}
public void release()
{
super.release();
hellomsg = null;
}
public void setHellomsg(String hellomsg)
{
this.hellomsg = hellomsg;
}
}
The first thing to note is the "hellomsg" bean property of type String
along with its associated getter and setter methods at the bottom of the class.
The hellomsg bean property, "hellomsg", is also an attribute in the
JSP tag. ()
Next we see two methods which associate this tag handler with our registered
UI Component: "tss.hello.JsfHello" as well as associate the renderer.
Since we don't have a separate renderer class, this statement returns a null
value.
// Associate the renderer and component type.
public String getComponentType() { return "tss.hello.JsfHello"; }
public String getRendererType() { return null; }
The next method, setProperties(), sets the incoming values from the JSP tag
by first calling the parent class' setProperties method along with custom code
to set the value from the hellomsg tag attribute. Notice that a check is first
done to see if the incoming attribute is a "ValueReference", which
takes the form of a JSF EL expression: #{Bean.Property}. If a "ValueReference"
is detected, it uses slightly different logic to set the application's value
binding.
protected void setProperties(UIComponent component)
{
super.setProperties(component);
// set hellomsg
if (hellomsg != null)
{
if (isValueReference(hellomsg))
{
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding(hellomsg);
component.setValueBinding("hellomsg", vb);
}
else
component.getAttributes().put("hellomsg", hellomsg);
}
}
Asides from the hellomsg getter and setter methods the only method required
in the taghandler class is the release() method which resets the bean properties
back to an unused state.
public void release()
{
super.release();
hellomsg = null;
}
and that's it for our tag handler class. Next we'll quickly review the JSP
Tag Library Descriptor (TLD) required for this tag.
Step 4. Building the Tag Library Descriptor File
In order for us to use our custom JSP tag handler class, we need to create
an associated TLD file which contains the tag entry associated with the tag
handler class. Here is an example of the TLD file needed for this tag. The TLD
associates the tag name, "jsfhello" with the tag class "tss.hello.FacesHelloTag"
along with it's associated attributes. For our tag, we include our custom attribute,
"hellomsg", along with the superclass' core attributes: "id",
"binding" and "rendered".
0.01
1.2
simple
http://theserverside.com/simplefacescomponents
This tag library contains simple JSF Component examples.
jsfhello
tss.hello.FacesHelloTag
binding
A value binding that points to a bean property
id
The client id of this component
rendered
Is this component rendered?
hellomsg
a custom message for the Component
Following standard J2EE architecture, the custom TLD file is placed in the
/WEB-INF/. sub-directory of the J2EE Web Module containing the application.
Running the simple HelloWorld Example
Just to review, our custom JSF component java classes must be compiled and
available on the classpath of a J2EE Web Module. The Web Module must also have
in it's class path the required JSF runtime jar files which include: jsf-api.jar,
jsf-impl.jar along with the commons jar files (commons-beanutils.jar,
commons-collections.jar, commons-digester.jar, commons-logging.jar).
The other two required jar files come from JSTL (jstl.jar and standard.jar).
For more info on the Commons and JSTL libraries refer to the Jakarta Apache
Project website:
http://jakarta.apache.org/
.) For a runtime deployment, all the required jar files must be placed in the
/WEB-INF/lib subdirectory of your J2EE Web Module.
Assuming your runtime environment is properly configured, you can test the
simple JSF Hello World custom component by first creating a JSF enabled JSP
page and then adding the taglib directive and dropping the tag into your page.
(As explained before..)
When the JSF page is run, you will see the execution of your HelloWorld JSF
Custom Component!
![]()
Extending the HelloWorld Custom Component
Now that we've created a simple HelloWorld component, we can now expand upon
this very easily. One straightforward extension would be to make the HelloWorld
component call a Web service for a specific stock symbol and print out it's
value. So instead of providing a hellomsg, our tag/component attribute will
contain a stock symbol. The renderer code will then query a stock quote Web
service (made available to us via another class) with the symbol provided by
the tag and print the current price in the response.
Here's how to modify the HelloWorld Custom Component to display a live stock
quote using a Web service.
Create a Web Service Proxy Client
In order to call a Web service from java, you'll need to create a proxy java
client which enables any other Java class the ability to call a stock Web service.
Many free stock quote Web services are available on the Internet. This example
uses the "Delayed Stock Quote Service" available from Xmethods.com.
The actual Web Proxy creation step is left to the reader to implement since
it is out of the bounds of JSF development however several Java Integrated Development
Environments such as Oracle JDeveloper, can automatically generate the necessary
client code which enables communication to a Web service.
![]()
Oracle JDeveloper 10.1.3 Preview's Web Service Proxy Wizard
Once a Web service Proxy class is created, we'll modify our hellomsg attribute
to become a stock symbol attribute and call the Web service with this value
and then print it out. This involves the following changes:
Create or Modify the HelloWorld UIComponent Class to Work with "Symbol"
Attribute
Change or create a new UIComponent class which refers to a stock symbol attribute,
"symbol", instead of the "hellomsg".
public class StockUIComp extends UIComponentBase
{
public void encodeEnd(FacesContext context) throws IOException
{
ResponseWriter writer = context.getResponseWriter();
String symbol = (String)getAttributes().get("symbol");
...
Note: If you create a new UI Component such as StockUIComp, don't forget to
register it in your faces-config file ( You could use the name: "tss.hello.JsfStock"
mapped to the classpath address "tss.hello.StockUIComp").
Once you've retrieved the attribute value for the stock symbol, you can call
your Web service proxy class to get the current price:
//get stock price using generated Web service Proxy
String stockprice = NetXmethodsServicesStockquoteStockQuotePortClient.getStockPrice(symbol);
Rendering the the stock price is easily done with the following code:
writer.startElement("p", this);
if(symbol != null)
writer.writeText("The stock price for " + symbol + " is: " + stockprice + ".", null);
else
writer.writeText("You must provide a Stock Symbol by setting the symbol attribute.", null);
writer.endElement("p");
The changes to the tag handler and TLD is a trivial search and replace of "hellomsg""
with "symbol". Your tag handler class will also have to refer to the
different Stock UI Component: "tss.helloJsfStock", but the remaining
code in the taghandler is basically the same as before with the "symbol"
attribute replacing the "hellomsg" attribute:
public class StockTag extends UIComponentTag
{
public String symbol = null;
public String getRendererType() { return null; }
public String getComponentType() { return "tss.hello.JsfStock"; }
protected void setProperties(UIComponent component)
{
super.setProperties(component);
// set symbol
if (symbol != null)
{
if (isValueReference(symbol))
{
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding(symbol);
component.setValueBinding("symbol", vb);
}
else
component.getAttributes().put("symbol", symbol);
}
...
Once you've updated your TLD with a new tag "simplestock" mapped
to the taghandler "StockTag", you can then add the new tag to your
JSP:
The SimpleStock UI Component:
Here's the output for the HelloWorld Component along with the new Stock UI
Component:
![]()
Extending the Stock UI Component Further
Now that we've extended the HelloWorld UI Component into something useful,
let's further extend the stock component to make it more user friendly. Our
modified stock component will now render an input field along with a submit
button. This will allow users to enter a new stock symbol at runtime as opposed
to hardcoding the attribute value in the JSP source.
![]()
This modification will introduce how to build a decode()encode()
method to now render in input field along with a submit button. method which
is used to "decode" the incoming form field values as opposed to simply
retrieving tag attribute values. We will also extend our simple
Back to the UI Component
This time we'll name the UI Component "HtmlInputStock" following
the JSF specification's naming convention. This time we'll extend extend the
class from the UIInput component since it will be accepting input.
public class HtmlInputStock extends UIInput
{
...
This component will have a more detailed encode() method. This time our encodeBegin()
method will call separate encode methods to render the differerent elements:
an input field, a submit button, and an output field which displays the returned
price of the stock. Notice that I've add the identifiers ".inputbutton"
and ".submitbutton" to the clientIds respectively when calling the
encode methods. This will ensure that the names of the rendered HTML: fields
will be unique on the client. In general this is good practice should you need
to refer to the rendered name of the HTML field.
The encodeBegin() method is as follows:
public void encodeBegin(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
String clientId = getClientId(context);
encodeInputField(writer, clientId+".inputbutton");
encodeSubmit(writer, clientId+".submitbutton");
encodeOutputField(context);
}
The encodeInputField() method is as follows:
private void encodeInputField(ResponseWriter writer, String clientId) throws IOException {
// render a standard HTML input field
writer.startElement("input", this);
writer.writeAttribute("type", "text", null);
writer.writeAttribute("name", clientId, "clientId");
Object v = getValue();
if (v != null)
writer.writeAttribute("value", v.toString(), "value");
writer.writeAttribute("size", "6", null);
writer.endElement("input");
}
Notice the name of the input field is assigned the clientId. We'll use this
later to identify the field when we decode the input.
The encodeSubmit() is rendered with:
private void encodeSubmit(ResponseWriter writer, String clientId) throws IOException {
// render a submit button
writer.startElement("input", this);
writer.writeAttribute("type", "Submit", null);
writer.writeAttribute("name", clientId, "clientId");
writer.writeAttribute("value", "Enter Stock Symbol", null);
writer.endElement("input");
}
And finally the encodeOutputField() which is similar to before:
public void encodeOutputField(FacesContext context) throws IOException
{
ResponseWriter writer = context.getResponseWriter();
String symbol = (String)getAttributes().get("value"); // The "value" attribute is used to pass the stock symbol.
//get stock price
String stockprice = NetXmethodsServicesStockquoteStockQuotePortClient.getStockPrice(symbol);
writer.startElement("p", this);
if(symbol != null)
writer.writeText("The current price for " + symbol + " is: " + stockprice + ".", null);
else
writer.writeText("", null);
writer.endElement("p");
}
Now let's move on to the decode() method who's job is to parse the incoming
field values and act upon it.
public void decode(FacesContext context) {
Map requestMap = context.getExternalContext().getRequestParameterMap();
String clientId = getClientId(context);
try {
String string_submit_val = ((String) requestMap.get(clientId+".inputfield"));
setSubmittedValue(string_submit_val);
setValid(true);
}
catch(NumberFormatException ex) {
// let the converter take care of bad input, but we still have
// to set the submitted value, or the converter won't have
// any input to deal with
setSubmittedValue((String) requestMap.get(clientId));
}
}
Let's review some of the key statements in our decode() method. First our decode()
method obtains a requestMap from the JSF Context.
Map requestMap = context.getExternalContext().getRequestParameterMap();
The requestMap is a container which allows us to access the values submitted
in the http request.
The next statement extracts the clientId which is the unique identifier of
the client issuing the request. (i.e. submitting the form)
String clientId = getClientId(context);
With the unique client identifier we can obtain the value of the submitted
value contained in the requestMap for this client. We can then set the submittedValue
to the value submitted in the form. Note: For this example, we are simply setting
the value that was submitted as is, but in other cases we may wish to perform
a conversion or validation on the submitted value before setting the value as
"valid".
try {
String string_submit_val = ((String) requestMap.get(clientId));
setSubmittedValue(string_submit_val);
setValid(true);
}
catch(NumberFormatException ex) {
setSubmittedValue((String) requestMap.get(clientId));
}
That takes care of our new and improved UIComponent rendering (encoding and
decoding). I'll leave out the step of registering this component in the faces-config
and highlight the remaining trivial code modifications in the tag handler and
the TLD.
For the tag handler we have only minor changes. This tag handler as before
extends from the same UIComponentTag but will have a bean property of "value"
instead of "symbol". The "value" property is the value submitted
in the input field and is handled exactly in the same manner before as the symbol.
The only other difference from before is that our tag handler is now registered
with the StockInput UI Component Class.
public String getComponentType() { return "tss.hello.StockInput"; }
The setProperties method is basically the same as before but operating on the
"value" instead "symbol":
protected void setProperties(UIComponent component)
{
super.setProperties(component);
if (value != null)
{
if (isValueReference(value))
{
FacesContext context = FacesContext.getCurrentInstance();
Application app = context.getApplication();
ValueBinding vb = app.createValueBinding(value);
component.setValueBinding("value", vb);
}
else
component.getAttributes().put("value", value);
}
Updating the TLD
Similar to before, the new TLD tag entry can be as follows:
...
stockinput
tss.hello.StockInputTag
binding
A value binding that points to a bean property
id
The client id of this component
rendered
Is this component rendered?
value
A value for submitted stock symbol..
....
Running the Stock Input Component
Invoking this component is done in a similar fashion as before where we simply
add the tag to the page:
Notice that we can leave the value attribute blank but specify it later when
we run it. Once the page is running, enter a test stock symbol such as ORCL
or AMZN and check out the result!
![]()
Summary
As you can see, building custom JSF UI Component is a fairly straightforward
process. Once you understand the mechanism detailed in the encode() and decode()
methods in the rendering code, the rest is a fairly trivial process of providing
the necessary "plumbing" with tag handlers as such to enable usage
in a typical client such as a JSP based client.
Future articles on JSF Component development will also build upon these simple
examples and show how to build other non visual JSF components such as Validators
and Converters.
Biography
Chris Schalk is a Principal Product Manager and Evangelist for Oracle JDeveloper
and is responsible for the Web application development features within JDeveloper.
Prior to product management, Chris held positions in both software development
and technical marketing at Oracle. Before Joining Oracle, he worked at IBM as
a software developer. Chris holds a Bachelor's of Science in Applied Mathematics
with a specialization in Computer Science from the University of California
at Los Angeles.
Chris has written numerous samples and articles for various publications including
Javapro, Oracle Magazine and also maintains a popular Blog on J2EE Web development
at:
http://jroller.com/page/cschalk
.
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/24141/showart_246374.html |
|