The life cycle of a Spring bean is easy to understand. When a bean is instantiated, it may be required to perform some initialization to get it into a usable state. Similarly, when the bean is no longer required and is removed from the container, some cleanup may be required.
Though, there is lists of the activities that take place behind the scenes between the time of bean Instantiation and its destruction, but this chapter will discuss only two important bean lifecycle callback methods which are required at the time of bean initialization and its destruction.
To define setup and teardown for a bean, we simply declare the <bean> with init-method and/ordestroy-method parameters. The init-method attribute specifies a method that is to be called on the bean immediately upon instantiation. Similarly, destroy-method specifies a method that is called just before a bean is removed from the container.
Initialization callbacks:
The org.springframework.beans.factory.InitializingBean interface specifies a single method:
void afterPropertiesSet()throwsException;
So you can simply implement above interface and initialization work can be done inside afterPropertiesSet() method as follows:
publicclassExampleBeanimplementsInitializingBean{publicvoid afterPropertiesSet(){// do some initialization work}}
In the case of XML-based configuration metadata, you can use the init-method attribute to specify the name of the method that has a void no-argument signature. For example:
publicclassExampleBean{publicvoid init(){// do some initialization work}}
Destruction callbacks
The org.springframework.beans.factory.DisposableBean interface specifies a single method:
void destroy()throwsException;
So you can simply implement above interface and finalization work can be done inside destroy() method as follows:
publicclassExampleBeanimplementsDisposableBean{publicvoid destroy(){// do some destruction work}}
In the case of XML-based configuration metadata, you can use the destroy-method attribute to specify the name of the method that has a void no-argument signature. For example:
publicclassExampleBean{publicvoid destroy(){// do some destruction work}}
If you are using Spring's IoC container in a non-web application environment; for example, in a rich client desktop environment; you register a shutdown hook with the JVM. Doing so ensures a graceful shutdown and calls the relevant destroy methods on your singleton beans so that all resources are released.
It is recommended that you do not use the InitializingBean or DisposableBean callbacks, because XML configuration gives much flexibility in terms of naming your method.
Example:
Let us have working Eclipse IDE in place and follow the following steps to create a Spring application:
Step
Description
1
Create a project with a name SpringExample and create a package com.tutorialspoint under the src folder in the created project.
2
Add required Spring libraries using Add External JARs option as explained in the Spring Hello World Example chapter.
3
Create Java classes HelloWorld and MainApp under the com.tutorialspoint package.
4
Create Beans configuration file Beans.xml under the src folder.
5
The final step is to create the content of all the Java files and Bean Configuration file and run the application as explained below.
Here is the content of HelloWorld.java file:
package com.tutorialspoint;publicclassHelloWorld{privateString message;publicvoid setMessage(String message){this.message = message;}publicvoid getMessage(){System.out.println("Your Message : "+ message);}publicvoid init(){System.out.println("Bean is going through init.");}publicvoid destroy(){System.out.println("Bean will destroy now.");}}
Following is the content of the MainApp.java file. Here you need to register a shutdown hookregisterShutdownHook() method that is declared on the AbstractApplicationContext class. This will ensures a graceful shutdown and calls the relevant destroy methods.
Once you are done with creating source and bean configuration files, let us run the application. If everything is fine with your application, this will print the following message:
Bean is going through init.
Your Message : Hello World!
Bean will destroy now.
Default initialization and destroy methods:
If you have too many beans having initialization and or destroy methods with the same name, you don't need to declare init-method and destroy-method on each individual bean. Instead framework provides the flexibility to configure such situation using default-init-method and default-destroy-methodattributes on the <beans> element as follows:
<beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"default-init-method="init"default-destroy-method="destroy"><beanid="..."class="..."><!-- collaborators and configuration for this bean go here --></bean></beans>
public class PersonValidatorTests extends TestCase { public void testEmptyPersonValidation() { Person person = new Person(); Validator validator = new PersonValidator(); BindException errors = new BindException(person, "target"); validator.validate(person, errors); new ErrorsVerifier(errors) { { forProperty("firstName").hasErrorCode("person.firstName.required") .forProperty("lastName").hasErrorCode("person.lastName.required") .otherwise().noErrors(); } } } }
spring-modules의 테스트는 spring의 컨텍스트 파일(xml 형식의) 설정을 통해 테스트를 한다. 그러나, 단위 테스트를 목적으로 한다면 외부 리소스에 의존하는 일은 가능한 제거하는 것이 좋다고 생각하여 위와 같이 직접 setValang()을 호출한 것이다. setValang()은 실제로는 다음과 같은 식으로 설정해야 한다.
<bean id="easyAddressValidator" class="org.springmodules.validation.ValangValidator "> <property name="valang"> <value><![CDATA[{ password : ? is not blank : '' : 'member.password.empty' }]]></value> </property> </bean>
spring-module의 의존성에 따라 테스트를 실행하려면, 다음 라이브러리가 필요하다.
commons-collections.jar
assertion을 위한 코드가 늘어서는 것도 가독성을 크게 저해하지는 않는다. 책에서의 의도와 비슷하게 Fluent Interface 스타일을 적용해서 조금 수정해봤다.
public void testPasswordValidation() throws Exception{
validator.setValang( "{ password : ? is not blank : '' : 'member.password.empty' }" ); validator.afterPropertiesSet(); // configure validation rules
validator.validate(member, errors);
new ErrorsVerifier(errors) .forProperty("password").hasErrorCode("member.password.empty");
}
class ErrorsVerifier{
private Errors errors; private String cursor; // one of successive properties
public ErrorsVerifier(BindException errors) { this.errors = errors; }
public ErrorsVerifier forProperty(String property) {
cursor = property; return this;
}
public ErrorsVerifier hasErrorCode(String errorCode) {
The Spring Modules project has a number of subprojects, including validation. This module is based on Spring ModulesValidation Version 0.9 and has a number of enhancements to the Valang part of the project.
Thanks to everyone that has worked on this project previously and currently.
1. Valang
Valang is short for Va-lidationLang-uage. It provides a very readable language for expressing validation rules, as well as providing the ability to add custom functions to the language. Once the ValangValidator is configured, using it isn't different than any other Spring Validator since it implements org.springframework.validation.Validator.
Below is a list of current enhancements.
Version 0.91
Bytecode generation added to DefaultVisitor as a replacement for reflection accessing simple properties (BeanPropertyFunction) for a significant performance improvement.
Basic enum comparison support. In the expression below the personType is an enum and the value STUDENT will be convereted to an enum for comparison. The value must match an enum value on the type being compared or an exception will be thrown.
personType EQUALS ['STUDENT']
For better performance the full class name can be specified so the enum can be retrieved during parsing. The first example is for standard enum and the second one is for an inner enum class .
Where clause support. In the expression below, the part of the expression price < 100 will only be evaluated if thepersonType is 'STUDENT'. Otherwise the validation will be skipped.
price < 100 WHERE personType EQUALS ['STUDENT']
Note
Support for the where clause has not been added to the JavaScript custom tag currently.
Improved performance of 'IN'/'NOT IN' if comparing a value to a java.util.Set it will use Set.contains(value). Static lists of Strings (ex: 'A', 'B', 'C') are now stored in a Set instead of an ArrayList.
Functions can be configured in Spring, but need to have their scope set as prototype and use a FunctionWrapper that is also a prototype bean with <aop:scoped-proxy> set on it.
Removed servlet dependency from Valang project except for the custom JSP tag ValangValidateTag needing it, but running Valang no longer requires it. This involved removing ServletContextAware from it's custom dependency injection. If someone was using this in a custom function, the function can now be configured directly in Spring and Spring can inject any "aware" values.
Changed logging to use SLF4J api.
Version 0.92
Removed custom dependency injection since functions can be configured in Spring.
Added auto-discovery of FunctionWrapper beans from the Spring context to go with existing auto-discovery of FunctionDefinition beans.
Version 0.93
Made specific comparison classes for each operator for a performance improvement.
Changed IS WORD and IS BLANK to use Commons Lang StringUtils, which will change the behavior slightly but should be more accurate to the description of the validation.
Change Operator from interfaces to an enum and removed OperatorConstants.
Fixed bytecode generation to handle a Map, a List, and an Array.
Version 0.94
Upgraded to Spring 3.0 and changed group & artifact IDs to match standard Spring naming conventions.
Version 0.95
Upgraded to Spring 3.1 and minor improvements to bytecode generation.
Rule Syntax
The basic construction of a Valang rule is to have it begin and end with a brace. Within the braces, the default property name for the rule is specified first. Then the Valang expression, followed by the default error message. These are all the required values for a Valang rule. The other optional values for a rule are the error message key and arguments for it. Each of the values of the rule are delimitted by a colon.
This is the default property of the bean being targeted for validation, and can be referred to with the shortcut ? in an expression.
true
expression
The Valang expression.
true
default-error-message
The default error message. If this isn't needed, it can be left blank even though it's required.
true
error-message-key
The message resource key for the i18n error message.
false
error-message-arg
If the error-message-key is specified, arguments for the error message can also be set as the final value of the rule. This accepts a comma delimited list of values.
false
Expression Syntax
The expression language provides an English like syntax for expressing validation rules. There are a number of operators for comparing a value to another. Logical expressions, resulting in true or false, can be grouped together with parentheses to form more complex expressions.
Just to give some context to the explanation of all the rules, below is a simple example. The bean being validated has the properties getFirstName(), getLastName(), and getAge(). The first two return a String and the last returns an int. The default property is 'firstName', which is referred to by the question mark. The first part of the rule enclosed in parentheses checks if the first name is either 'Joe' or it's length is greater than 5. The next part checks if the last name is one of the values in the list, and the final part checks if the age is over 18.
(? EQUALS 'Joe' OR length(?) > 5) AND lastName IN 'Johnson', 'Jones', 'Smith' AND age > 18
Operator Syntax
The parser is not case sensitive when processing the operators.
Table 2. Expression Operators
Comparison Operator
Description
Supports
Example
= | == | IS | EQUALS
Checks for equality.
Strings, booleans, numbers, dates, and enums.
firstName EQUALS 'Joe'
!= | <> | >< | IS NOT | NOT EQUALS
Checks for inequality.
Strings, booleans, numbers, dates, and enums.
firstName NOT EQUALS 'Joe'
> | GREATER THAN | IS GREATER THAN
Checks if a value is greater than another.
Numbers and dates.
age > 18
< | LESS THAN | IS LESS THAN
Checks if a value is less than another.
Numbers and dates.
age > 18
>= | => | GREATER THAN OR EQUALS | IS GREATER THAN OR EQUALS
Checks if a value is greater than or equal to another.
Numbers and dates.
age >= 18
<= | =< | LESS THAN OR EQUALS | IS LESS THAN OR EQUALS
Checks if a value is less than or equal to another.
Numbers and dates.
age <= 18
NULL | IS NULL
Checks if a value is null.
Objects.
firstName IS NULL
NOT NULL | IS NOT NULL
Checks if a value is not null.
Objects.
firstName IS NOT NULL
HAS TEXT
Checks if the value has at least one non-whitespace character.
Strings.
firstName HAS TEXT
HAS NO TEXT
Checks if the value doesn't have a non-whitespace character.
Strings.
firstName HAS NO TEXT
HAS LENGTH
Checks if the value's length is greater than zero.
Strings.
firstName HAS LENGTH
HAS NO LENGTH
Checks if the value's length is zero.
Strings.
firstName HAS NO LENGTH
IS BLANK
Checks if the value is blank (null or zero length).
Strings.
firstName IS BLANK
IS NOT BLANK
Checks if the value isn't blank (not null, length greater than zero).
Strings.
firstName IS NOT BLANK
IS UPPERCASE | IS UPPER CASE | IS UPPER
Checks if the value is uppercase.
Strings.
firstName IS UPPERCASE
IS NOT UPPERCASE | IS NOT UPPER CASE | IS NOT UPPER
Checks if the value isn't uppercase.
Strings.
firstName IS NOT UPPERCASE
IS LOWERCASE | IS LOWER CASE | IS LOWER
Checks if the value is lowercase.
Strings.
firstName IS LOWERCASE
IS NOT LOWERCASE | IS NOT LOWER CASE | IS NOT LOWER
Checks if the value isn't lowercase.
Strings.
firstName IS NOT LOWERCASE
IS WORD
Checks if the value has one or more letters or numbers (no spaces or special characters).
Strings.
firstName IS WORD
IS NOT WORD
Checks if the value doesn't have one or more letters or numbers (no spaces or special characters).
Strings.
firstName IS NOT WORD
BETWEEN
Checks if a value is between two other values.
Numbers and dates.
age BETWEEN 18 AND 65
NOT BETWEEN
Checks if a value isn't between two other values.
Numbers and dates.
age NOT BETWEEN 18 AND 65
IN
Checks if a value is in a list.
Strings, booleans, numbers, dates, and enums.
firstName IN 'Joe', 'Jack', 'Jane', 'Jill'
NOT IN
Checks if a value isn't in a list.
Strings, booleans, numbers, dates, and enums.
firstName NOT IN 'Joe', 'Jack', 'Jane', 'Jill'
NOT
Checks for the opposite of the following expression.
Any expression.
NOT firstName EQUALS 'Joe'
!
Changes a boolean expression to it's opposite.
Booleans
matches('\\s+', firstName) IS !(TRUE)
AND
Used to join together the logical comparisons on either side of the operator. Both must evaluate totrue.
Any expression.
firstName EQUALS 'Joe' AND age > 21
OR
Used to join together the logical comparisons on either side of the operator. Only one must evaluate totrue.
Any expression.
firstName EQUALS 'Joe' OR age > 21
WHERE
If the where expression is true, then the main expression for validation is performed. Otherwise it isn't evaluated and no errors are generated.
Any expression.
firstName EQUALS 'Joe' WHERE age > 21
this
A reference to the bean passed in for validation, which could be passed into a custom function for example.
Any expression.
isValid(this) IS TRUE
Literal Syntax
Table 3. Literals
Literal Type
Description
Example
String
String literals are surrounded by single quotes.
'Joe'
Numbers
Numbers can be expressed without any special syntax. Numbers are all parsed usingBigDecimal.
1, 100, 0.73, -2.48
Dates
Date literals are surrounded by brackets.
These are the supported formats supported by the DefaultDateParser.
There are four different constants for boolean values. The values 'TRUE' and 'YES' represent true, and the values 'FALSE' and 'NO' represent false
TRUE, YES, FALSE, NO
Enums
Enums are surrounded by bracket and single quotes. If the full path to the enum isn't specified, it will be resolved when the expression is evaluated by looking up the enum value from enum on the opposite side of the expression.
Valang supports basic mathematical formulas based on numeric literals and property values.
Table 4. Mathematical Expression Operators
Mathematical Operator
Description
Example
+
Addition operator.
price + 12
-
Subtraction operator.
price - 12
*
Multiplication operator.
price * 1.2
/ | DIV
Division operator.
price / 2
% | MOD
Modulo operator.
age % 10
Property Syntax
Valang supports standard property and nested property access to the bean passed in for validation.
Table 5. Property Syntax
Property Type
Description
Example
Standard
Using standard JavaBean property notation, a value from the bean being validated may be retrieved. The address represents getAddress() on the bean.
address IS NOT NULL
Nested
Using standard JavaBean property notation, a nested value from the bean being validated may be retrieved. The address.city represents getAddress().getCity() on the bean.
address.city IS NOT BLANK
List
From an array, List, or Set, a value from it can be returned by specifying it's index. Only arrays and lists are supported by bytecode generation.
addresses[1] IS NOT NULL
Map
From a Map, the value based on the key specified is retrieved.
addresses[home] IS NOT NULL
Functions
These are built in functions that come with Valang. The function framework is pluggable, so it's easy to add custom functions. Adding custom functions will be covered in the next section.
Table 6. Functions
Function
Description
Example
length | len | size | count
Returns the size of a collection or an array, and otherwise returns the length of string by called toString() on the object.
length(firstName) < 20
match | matches
Performs a match on a regular expression. The first argument is the regular expression and the second is the value match on.
Checks if the user authenticated by Spring Security is in a role.
inRole('ADMIN') IS TRUE
Custom Functions
Custom functions can either be explicitly registered or instances of FunctionDefinition and FunctionWrapper are automatically registered with a ValangValidator. If just specifying a class name, it must have a constructor with the signature Function[] arguments, int line, int column. The FunctionWrapper is specifically for Spring configured functions. If the Function in a FunctionWrappertakes any arguments, it must implement ConfigurableFunction which allows the parser to configure the arguments, line number, and column number. Otherwise the line & column number will not be set on a Spring configured function.
Note
It's important for a FunctionWrapper around a custom Function to be of the scope prototype as well as theFunctionWrapper. Also the FunctionWrapper must have <aop:scoped-proxy/> defined so each call to it will get a new instance of the function. This is because as the validation language is parsed a new instance of a function is made each time and has the arguments specific to that function set on it.
Spring Configuration
The example below shows how to explicitly register a custom function directly with a validator. The custom functions 'validLastName' and 'creditApproval' are registered on the customFunctions property as a Map. The key is the name of the function to be used in the validation language and the value if the function being registered, which can either be the fully qualified name of the class or an instance of FunctionWrapper.
ValangValidatorCustomFunctionTest-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="creditApprovalFunction"
class="org.springmodules.validation.valang.CreditApprovalFunction"
scope="prototype">
<property name="creditRatingList">
<list>
<value>GOOD</value>
<value>EXCELLENT</value>
</list>
</property>
</bean>
<bean id="personValidator" class="org.springmodules.validation.valang.ValangValidator">
<property name="className" value="org.springmodules.validation.valang.Person"/>
<property name="customFunctions">
<map>
<entry key="validLastName">
<value>org.springmodules.validation.valang.ValidLastNameFunction</value>
</entry>
<entry key="creditApproval">
<bean class="org.springmodules.validation.valang.functions.FunctionWrapper"
scope="prototype">
<aop:scoped-proxy/>
<property name="function" ref="creditApprovalFunction" />
</bean>
</entry>
</map>
</property>
<!--
Final validation tests that the aop:scoped-proxy is working since if the same instance
of CreditApprovalFunction is used it will be set to a failing value for both sides of the or.
While if two instances are made the first condition should pass while the second will fail.
-->
<property name="valang">
<value><![CDATA[
{ lastName : validLastName(?) is true : '' }
{ lastName : creditApproval(age, creditRating) is true : '' }
{ lastName : validLastName(?) is true AND creditApproval(age, creditRating) is true : '' }
{ lastName : validLastName(?) is true AND
(creditApproval(age, creditRating) is true OR
creditApproval(age, ['org.springmodules.validation.valang.Person$CreditRating.FAIR']) is true) : '' }
]]<</value>
</property>
</bean>
</beans>
Instances of FunctionDefinition and FunctionWrapper are automatically registered with a ValangValidator The custom functions 'validLastName' and 'creditApproval' are registered. If a FunctionWrapper doesn't have a function name specified, the name of the bean will be used for the function name.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springmodules.validation.valang.functions.FunctionDefinition"
p:name="validLastName"
p:className="org.springmodules.validation.valang.ValidLastNameFunction"/>
<!-- Uses bean name for function name if not explicitly set on the wrapper -->
<bean id="creditApproval"
class="org.springmodules.validation.valang.functions.FunctionWrapper"
scope="prototype">
<aop:scoped-proxy/>
<property name="function">
<bean id="creditApprovalFunction"
class="org.springmodules.validation.valang.CreditApprovalFunction"
scope="prototype">
<property name="creditRatingList">
<list>
<value>GOOD</value>
<value>EXCELLENT</value>
</list>
</property>
</bean>
</property>
</bean>
<bean id="personValidator" class="org.springmodules.validation.valang.ValangValidator">
<property name="className" value="org.springmodules.validation.valang.Person"/>
<!--
Final validation tests that the aop:scoped-proxy is working since if the same instance
of CreditApprovalFunction is used it will be set to a failing value for both sides of the or.
While if two instances are made the first condition should pass while the second will fail.
-->
<property name="valang">
<value><![CDATA[
{ lastName : validLastName(?) is true : '' }
{ lastName : creditApproval(age, creditRating) is true : '' }
{ lastName : validLastName(?) is true AND creditApproval(age, creditRating) is true : '' }
{ lastName : validLastName(?) is true AND
(creditApproval(age, creditRating) is true OR
creditApproval(age, ['org.springmodules.validation.valang.Person$CreditRating.FAIR']) is true) : '' }
]]<</value>
</property>
</bean>
</beans>
Code Example
Checks if the last name is in a list, and if it isn't false is returned.
Example 1. ValidLastNameFunction
public class ValidLastNameFunction extends AbstractFunction {
final Logger logger = LoggerFactory.getLogger(ValidLastNameFunction.class);
final Set<String> lValidLastNames = new HashSet<String>();
/**
* Constructor
*/
public ValidLastNameFunction(Function[] arguments, int line, int column) {
super(arguments, line, column);
definedExactNumberOfArguments(1);
lValidLastNames.add("Anderson");
lValidLastNames.add("Jackson");
lValidLastNames.add("Johnson");
lValidLastNames.add("Jones");
lValidLastNames.add("Smith");
}
/**
* Checks if the last name is blocked.
*
* @return Object Returns a <code>boolean</code> for
* whether or not the last name is blocked.
*/
@Override
protected Object doGetResult(Object target) {
boolean result = true;
String symbol = getArguments()[0].getResult(target).toString();
if (!lValidLastNames.contains(symbol)) {
result = false;
}
return result;
}
}
The function checks if a person can get credit approval. Their credit rating is checked against a list only if they are over 18 years old.
Example 2. ConfigurableFunction
public class CreditApprovalFunction extends AbstractFunction
implements ConfigurableFunction {
final Logger logger = LoggerFactory.getLogger(CreditApprovalFunction.class);
Set<Person.CreditRating> lCreditRatings = new HashSet<Person.CreditRating>();
/**
* Constructor
*/
public CreditApprovalFunction() {}
/**
* Constructor
*/
public CreditApprovalFunction(Function[] arguments, int line, int column) {
super(arguments, line, column);
definedExactNumberOfArguments(2);
lCreditRatings.add(Person.CreditRating.FAIR);
lCreditRatings.add(Person.CreditRating.GOOD);
lCreditRatings.add(Person.CreditRating.EXCELLENT);
}
/**
* Gets number of expected arguments.
* Implementation of <code>ConfigurableFunction</code>.
*/
public int getExpectedNumberOfArguments() {
return 2;
}
/**
* Sets arguments, line number, and column number.
* Implementation of <code>ConfigurableFunction</code>.
*/
public void setArguments(int expectedNumberOfArguments, Function[] arguments,
int line, int column) {
// important to set template first or can cause a NullPointerException
// if number of arguments don't match the expected number since
// the template is used to create the exception
super.setTemplate(line, column);
super.setArguments(arguments);
super.definedExactNumberOfArguments(expectedNumberOfArguments);
}
/**
* Sets valid credit rating approval list.
*/
public void setCreditRatingList(Set<Person.CreditRating> lCreditRatings) {
this.lCreditRatings = lCreditRatings;
}
/**
* If age is over 18, check if the person has good credit,
* and otherwise reject.
*
* @return Object Returns a <code>boolean</code> for
* whether or not the person has good enough
* credit to get approval.
*/
@Override
protected Object doGetResult(Object target) {
boolean result = true;
int age = (Integer) getArguments()[0].getResult(target);
Person.CreditRating creditRating = (Person.CreditRating)getArguments()[1].getResult(target);
// must be over 18 to get credit approval
if (age > 18) {
if (!lCreditRatings.contains(creditRating)) {
result = false;
}
}
return result;
}
}
Bytecode Generation
If the validator will only be used to validate a specific class, the property 'className' can be specified to avoid reflection. If it's set, a custom Function will be generated that directly retrieves a property to avoid reflection. This provides a significant performance improvement if that is a concern, which typically isn't if the validation is being used to validate a web page since the delay is so small either way.
Note
Only a Map, a List, or an Array is supported by bytecode generation, not a Set. Primitive arrays currently aren't supported, but any object one is. Also, nested properties are currently not supported.
This is a small excerpt from the logging of the performance unit test. As you can see from the logging, as the validator is initialized it generates bytecode and shows for which class and method, as well as what the generated class name is. The package and name of the original class is used and then has what property is being retrieved appended along with 'BeanPropertyFunction$$Valang' to make a unique class name to try to avoid any collisions.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getLastName()
as 'org.springmodules.validation.valang.PersonLastNameBeanPropertyFunction$$Valang'.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getAge()
as 'org.springmodules.validation.valang.PersonAgeBeanPropertyFunction$$Valang'.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getCreditRating()
as 'org.springmodules.validation.valang.PersonCreditRatingBeanPropertyFunction$$Valang'.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getFirstName()
as 'org.springmodules.validation.valang.PersonFirstNameBeanPropertyFunction$$Valang'.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getCreditStatus()
as 'org.springmodules.validation.valang.PersonCreditStatusBeanPropertyFunction$$Valang'.
ValangValidatorPerformanceTest - Took 7098.0ns.
ValangValidatorPerformanceTest - Took 2124.0ns.
ValangValidatorPerformanceTest - Message validator took 7098.0ns, and bytecode message valdiator took 2124.0ns.
Results from ValangValidatorPerformanceTest which was run on a Macbook Pro (2.3GHz Intel Core i7 with 8 GB RAM with OS X 10.6.8) with Java 6. All the expressions are identical, but adjusted to either retrieve the values being compared from a JavaBean, Map, List, or an array.
{ lastName : validLastName(?) is true AND creditApproval(age, creditRating) is true WHERE firstName IN 'Joe', 'Jack', 'Jill', 'Jane' AND creditStatus IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND creditRating EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND age > 18 : '' }
1176ns
327ns
{ mapVars[lastName] : validLastName(?) is true AND creditApproval(mapVars[age], mapVars[creditRating]) is true WHERE mapVars[firstName] IN 'Joe', 'Jack', 'Jill', 'Jane' AND mapVars[creditStatus] IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND mapVars[creditRating] EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND mapVars[age] > 18 : '' }
905ns
48ns
{ listVars[1] : validLastName(?) is true AND creditApproval(listVars[2], listVars[4]) is true WHERE listVars[0] IN 'Joe', 'Jack', 'Jill', 'Jane' AND listVars[3] IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND listVars[4] EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND listVars[2] > 18 : '' }
575ns
43ns
{ vars[1] : validLastName(?) is true AND creditApproval(vars[2], vars[4]) is true WHERE vars[0] IN 'Joe', 'Jack', 'Jill', 'Jane' AND vars[3] IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND vars[4] EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND vars[2] > 18 : '' }
563ns
40ns
Spring Configuration
By specifying the 'className' property, bytecode will be generated for each method being called to avoid reflection. This gives a significant performance improvement.
Excerpt from ValangValidatorCustomFunctionTest-context.xml
<!--
Only perform validation if valid first name, credit status is failed or pending,
and the credit rating is excellent where the person's age is over 18.
-->
<bean id="expression" class="java.lang.String">
<constructor-arg>
<value><![CDATA[
{ lastName : validLastName(?) is true AND creditApproval(age, creditRating) is true
WHERE firstName IN 'Joe', 'Jack', 'Jill', 'Jane' AND
creditStatus IN ['org.springmodules.validation.valang.CreditStatus.PENDING'],
['org.springmodules.validation.valang.CreditStatus.FAIL'] AND
creditRating EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND
age > 18 : '' }
]]<</value>
</constructor-arg>
</bean>
...
<bean id="bytecodePersonValidator" class="org.springmodules.validation.valang.ValangValidator">
<property name="className" value="org.springmodules.validation.valang.Person"/>
<property name="valang" ref="expression" />
</bean>
Date Examples
The default date parser provides support for a number of different date literals, and also has support for shifting and manipulating dates. Below are a few examples, but see the DefaultDateParser for more detailed information.
■ ApplicationContext 클래스 ○ 메시지 처리 - MessageSource 인터페이스를 상속 받고 있음. (getMessage() 메서드를 이용하여 ApplicationContext로부터 지역 및 언어에 알맞은 메시지를 가져올 수 있음.) - 등록된 빈 객체 중에서 이름이 'messageSource'인 MessageSource 타입의 빈 객체를 이용하여 메시지를 가져옴. ○ 'messageSource'가 이름인 빈 객체 정의
- basename 프로퍼티의 값은 메시지를 로딩할 때 사용할 ResourceBundle 의 베이스 이름. (베이스 이름 : 패키지를 포함한 완전한 이름이어야 함.) - message.greeting 값은 message 패키지에 있는 greeting 프로퍼티 파일로부터 메시지를 가져옴. ■ ResourceBundleMessageSource 클래스 ○ 정의 - MessageSource 인터페이스의 구현 클래스. - java.util.ResourceBundle을 이용하여 메시지를 읽어오는 MessageSource 구현체. ○ 한 개 이상의 프로퍼티 파일로부터 메시지 로딩 - <list> 태그 이용.
■ 프로퍼티 파일 ○ 특징 - ResourceBundle은 프로퍼티 파일의 이름을 이용하여 언어 및 지역에 따른 메시지를 로딩. ○ message 프로퍼티 파일 파일명 - message.properties : 기본 메시지. 시스템의 언어 및 지역에 맞는 프로퍼티 파일이 존재하지 않을 경우에 사용. - message_en.properties : 영어 메시지. - message_ko.properties : 한글 메시지. - message_en_UK.properties : 영국을 위한 영어 메시지 ○ message 프로퍼티 파일 - 각 프로퍼티 파일은 해당 언어에 알맞은 메시지를 포함.
/* message_en.properties */
greeting = Hello!
/* message_ko.properties */
<!-- '안녕하세요!'를 유니코드 값으로 변환한 값. --> greeting = \uc548\ub155\ud558\uc138\uc694!
■ 메시지 로딩 - ApplicationContext.getMessage() 메서드를 이용.
Local locale = Locale.getDefault(); String greeting = context.getMessage("greeting", new Object[0], locale);
웹브라우저의 요청이 들어오면 DispatcherServlet의 객체가 이를 받는다. DispatcherServlet의 객체는 다시HandlerMapping객체를 통해(참조하여) 어떤 Controller에게 처리를 위임해야할지를 통보받아서 그 Controller객체에게 처리를 위임한다.
Controller객체는 모델(Service-DAO)단과 통신을 하여 비지니스 로직을 호출하고 그 결과를 ModelAndView객체로 다시 DispatcherServlet객체로 반환한다.
마지막으로 DispatcherServlet객체는 ViewResolver객체에게 사용할 View객체를 반환받아 그 View객체에 Controller객체가 반환한 정보(ModelAndView)를 포함시켜 그 결과를 브라우즈에 반환한다.
스프링 프레임워크는 DI나 AOP와 같은 기능 뿐 아니라 기본적으로 웹 개발을 위한 MVC 프레임워크도 함께 제공하고 있다. 스프링 MVC 프레임워크는 스프링을 기반으로 하고 있기 때문에 스프링이 제공하는 트랜잭션 처리나 DI 및 AOP 적용 등을 손쉽게 사용할 수 있다는 장점을 갖는다.
또한 스트럿츠와 같은 프레임워크와 스프링 프레임워크를 연동하기 위해 추가적인 설정을 하지 않아도된다는 장점을 가지고 있다
1. 스프링 MVC의 주요 구성요소
DispatcherServlet
클라이언트의 요청을 전달받는다. 컨트롤러에게 클라이언트의 요청을 전달하고 컨트롤러가 리턴한 결과 값을 View에 전달하여 알맞은 응답을 생성하도록 한다.
HandlerMapping
클라이언트의 요청 URL을 어떤 컨트롤러가 처리할지 결정한다
컨트롤러(Controller)
클라이언트의 요청을 처리한 뒤 그 결과를 DispatcherServlet에 알려준다. 스트럿츠의 Action과 동일한 역할을 수행한다.(ModelAndView 리턴)
ModelAndView
컨트롤러가 처리한 결과 정보 및 뷰 선택에 필요한 정보를 담는다
ViewResolver
컨트롤러의 처리 결과를 생성할 뷰를 선택한다
View
컨트롤러의 처리 결과 화면을 생성한다
구성요소간 프로세스 흐름
1. 클라이언트의 요청이 DispatcherServlet에 전달된다
2. DispatcherServlet은 HandlerMapping을 사용하여 클라이언트의 요청을 처리할 컨트롤러 객체를 구한다
3. DispatcherServlet은 컨트롤러 객체의 HandleRequest() 메서드를 호출하여 클라이언트의 요청을 처리한다.
4. 컨트롤러의 handleRequest() 메서드는 처리 결과 정보를 담은 ModelAndView 객체를 리턴한다
1. 클라이언트의 요청을 받을 DispatcherServlet을 web.xml 파일에 설정한다.
2. HandlerMapping을 이용하여 요청 URL과 컨트롤러의 매핑방식을 결정한다.
3. 클라이언트의 요청을 처리할 컨트롤러를 작성한다.
4. 어떤 뷰를 이용하여 컨트롤러의 처리 결과 응답 화면을 생성할지 결정하는 ViewResolver를 설정한다
5. JSP나 Velocity 등을 이용하여 뷰 영역의 코드를 작성한다
2.1 단계 1, DispatcherServlet 설정 및 스프링컨텍스트 설정
스프링 MVC를 사용하기에 앞서 가정 먼저 해야 할 작업은 자바 웹 어플리케이션의 설정 파일인 web.xml 파일에 다음의 두 가지 정보를 추가하는 것이다
(1) 클라이언트의 요청을 전달받을 DispatcherServlet 설정
(2) 공통으로 사용할 어플리케이션 컨텍스트 설정
DispatcherServlet 은 클라이언트의 요청을 전달받는 서블릿으로서 컨트롤러나 뷰와 같은 스프링 MVC의 구성요소를 이용하여 클라이언트에게 서비스를 제공하게 된다. DispatcherServlet의 설정은 웹 어플리케이션의 /WEB-INF/web.xml 파일에 추가하며 다음과 같이 서블릿과 서블릿 매핑 정보를 추가하면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>
org.springframework.weg.servlet.DispatcherServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
</weg-app>
위 코드에서는 *.htm으로 들어오는 클라이언트의 요청을 DispatcherServlet이 처리하도록 설정하였다.
DispatcherServlet은 WEB-INF/ 디렉터리에 위치한 [서블릿이름]-servlet.xml 파일을 스프링 설정 파일로 사용한다. 예를 들어 위 코드의 경우 dispatcher-servlet.xml 파일을 설정 파일로 사용하게 된다. 이 파일에서 스프링 MVC의 구성요소인 HandlerMapping, Controller, ViewResolver, View등의 빈을 설정하게 된다.
2.2 단계2, 설정 파일에 HandlerMapping 설정 추가
HandlerMapping은 클라이언트의 요청을 어떤 컨트롤러가 처리할지에 대한 정보를 제공한다. 스프링은 기본적으로 몇 가지 HandlerMapping을 제공하고 있는데, 여기서는 BeanNameUrlHandlerMapping을 사용할 것이다.
DispatcherServlet이 사용하는 스프링 설정 파일에(예를 들어, 이번 예제에서는 dispatcher-servlet.xml) 다음과 같이 사용할 HandlerMapping을 빈으로 등록한다
<bean id="beanNameUrlMapping"
class = "org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
BeanNameUrlHandlerMapping은 간단하게 설명하면 클라이언트의 요청 URL과 동일한 이름을 갖는 빈을 컨트롤러로 사용하도록 매핑한다.
2.3 단계3, 컨트롤러 구현 및 설정
스프링은 Controller 인터페이스를 제공하고 있으며, 컨트롤러 클래스는 Controller 인터페이스를 구현하면 된다. 실제로는 Controller 인터페이스를 직접 구현하는 경우는 드물며, 스프링이 제공하는 몇 가지 기본 컨트롤러 클래스 중에서 알맞은 클래스를 상속받아 구현하게 된다
앞 예제에서 ModelAndView 객체에 "greeting"이라는 이름으로 값을 저장하였다
InternalResourceView는 ModelAndView에 저장된 객체를 HttpServletRequest 객체의 속성(attribute)에 그대로 저장한다. 따라서 JSP에서는 ${greeting}과 같은 표현언어를 이용하여 ModelAndView에 저장된 값에 접근할 수 있다 또는 다음과 같이 저장된 객체를 구할 수도 있다