Spring Framework разработан на основе двух основных концепций – Внедрение зависимостей и Программирование с использованием аспектов (Spring AOP).
Spring AOP
Мы уже видели, как работает Внедрение зависимостей в Spring, сегодня мы рассмотрим основные концепции программирования с использованием аспектов и как их можно реализовать с помощью Spring Framework.
Обзор Spring AOP
Большинство корпоративных приложений имеют некоторые общие аспекты, которые применимы к различным типам объектов и модулей. Некоторыми из общих аспектов являются ведение журнала, управление транзакциями, проверка данных, и так далее. В объектно-ориентированном программировании модульность приложения достигается с помощью классов, тогда как в аспектно-ориентированном программировании модульность приложения достигается с помощью аспектов, которые настраиваются для проникновения через различные классы. Spring AOP устраняет прямую зависимость от поперечных задач в классах, которую нельзя достичь с помощью обычной объектно-ориентированной модели программирования. Например, мы можем иметь отдельный класс для ведения журнала, но функциональные классы все равно должны вызывать эти методы, чтобы обеспечить ведение журнала по всему приложению.
Основные понятия аспектно-ориентированного программирования
Прежде чем мы погрузимся в реализацию Spring AOP, мы должны понять основные понятия AOP.
- Аспект: Аспект – это класс, который реализует аспекты корпоративных приложений, проникающие через несколько классов, такие как управление транзакциями. Аспекты могут быть обычным классом, настраиваемым через конфигурацию Spring XML, или мы можем использовать интеграцию Spring AspectJ, чтобы определить класс как аспект с помощью аннотации
@Aspect
. - Точка соединения: Точка соединения – это определенная точка в приложении, такая как выполнение метода, обработка исключений, изменение значений переменных объекта и т. д. В Spring AOP точка соединения всегда представляет собой выполнение метода.
- Совет: Советы – это действия, выполняемые для определенной точки соединения. В терминах программирования это методы, которые выполняются, когда в приложении достигается определенная точка соединения с соответствующим pointcut. Вы можете представлять себе советы как перехватчики Struts2 или фильтры Servlet.
- Точка среза: Точка среза – это выражения, сопоставляемые с точками соединения для определения того, нужно ли выполнять совет или нет. Точка среза использует различные виды выражений, которые сопоставляются с точками соединения, и Spring Framework использует язык выражений точек AspectJ.
- Целевой объект: Это объект, на который применяются советы. В Spring AOP используются прокси-объекты времени выполнения, поэтому этот объект всегда является прокси-объектом. Это означает, что подкласс создается во время выполнения, где целевой метод переопределяется, а советы включаются на основе их конфигурации.
- Прокси AOP: Реализация Spring AOP использует динамический прокси JDK для создания классов Proxy с целевыми классами и вызовами советов, которые называются классами AOP proxy. Мы также можем использовать прокси CGLIB, добавив его в зависимости проекта Spring AOP.
- Переплетение: Это процесс связывания аспектов с другими объектами для создания советованных прокси-объектов. Это можно сделать на этапе компиляции, загрузки или во время выполнения. Spring AOP выполняет переплетение во время выполнения.
Типы советов AOP
В зависимости от стратегии выполнения советов они делятся на следующие типы.
- Совет перед выполнением (Before Advice): Эти советы выполняются перед выполнением методов точек соединения. Мы можем использовать аннотацию
@Before
для обозначения типа совета как совет перед выполнением. - Совет после выполнения (After (finally) Advice): Совет, который выполняется после завершения выполнения метода точки соединения, нормально или при возникновении исключения. Мы можем создать совет после выполнения с использованием аннотации
@After
. - Совет после возврата (After Returning Advice): Иногда мы хотим, чтобы методы советов выполнялись только в случае нормального выполнения метода точки соединения. Мы можем использовать аннотацию
@AfterReturning
для обозначения метода как совет после возврата. - После Выбрасывания Совета: Этот совет выполняется только когда метод точки присоединения выбрасывает исключение, мы можем использовать его для отката транзакции декларативно. Мы используем аннотацию
@AfterThrowing
для этого типа совета. - Около Совета: Это самый важный и мощный совет. Этот совет окружает метод точки присоединения, и мы также можем выбирать, выполнять ли метод точки присоединения или нет. Мы можем написать код совета, который выполняется до и после выполнения метода точки присоединения. Обязанностью около совета является вызов метода точки присоединения и возвращение значений, если метод что-то возвращает. Мы используем аннотацию
@Around
для создания методов около советов.
Упомянутые выше точки могут показаться запутанными, но когда мы рассмотрим реализацию Spring AOP, все станет более ясно. Давайте начнем создавать простой проект Spring с реализациями AOP. Spring предоставляет поддержку использования аннотаций AspectJ для создания аспектов, и мы будем использовать это для простоты. Все вышеупомянутые аннотации AOP определены в пакете org.aspectj.lang.annotation
. Spring Tool Suite предоставляет полезную информацию об аспектах, поэтому я бы порекомендовал вам использовать его. Если вы не знакомы с STS, я бы порекомендовал вам взглянуть на Учебник Spring MVC, где я объяснил, как его использовать.
Пример Spring AOP
Создайте новый простой проект Simple Spring Maven так, чтобы все библиотеки Spring Core были включены в файлы pom.xml, и нам не нужно было бы включать их явно. Наш конечный проект будет выглядеть как на изображении ниже, мы рассмотрим основные компоненты Spring и подробно изучим реализацию аспектов.
Зависимости Spring AOP AspectJ
Spring framework предоставляет поддержку AOP по умолчанию, но поскольку мы используем аннотации AspectJ для конфигурации аспектов и советов, нам нужно включить их в файл pom.xml.
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.samples</groupId>
<artifactId>SpringAOPExample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<!-- Generic properties -->
<java.version>1.6</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Spring -->
<spring-framework.version>4.0.2.RELEASE</spring-framework.version>
<!-- Logging -->
<logback.version>1.0.13</logback.version>
<slf4j.version>1.7.5</slf4j.version>
<!-- Test -->
<junit.version>4.11</junit.version>
<!-- AspectJ -->
<aspectj.version>1.7.4</aspectj.version>
</properties>
<dependencies>
<!-- Spring and Transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- Logging with SLF4J & LogBack -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
<!-- AspectJ dependencies -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</project>
Обратите внимание, что я добавил зависимости aspectjrt
и aspectjtools
(версия 1.7.4) в проект. Также я обновил версию Spring framework до самой последней на момент создания, т.е. 4.0.2.RELEASE.
Класс модели
Давайте создадим простой Java bean, который мы будем использовать для нашего примера с несколькими дополнительными методами. Код Employee.java:
package com.journaldev.spring.model;
import com.journaldev.spring.aspect.Loggable;
public class Employee {
private String name;
public String getName() {
return name;
}
@Loggable
public void setName(String nm) {
this.name=nm;
}
public void throwException(){
throw new RuntimeException("Dummy Exception");
}
}
Вы заметили, что метод setName() аннотирован аннотацией Loggable
. Это пользовательская аннотация Java, которую мы определили в проекте. Позже мы рассмотрим её использование.
Класс Сервиса
Давайте создадим сервисный класс для работы с бином сотрудника. Код EmployeeService.java:
package com.journaldev.spring.service;
import com.journaldev.spring.model.Employee;
public class EmployeeService {
private Employee employee;
public Employee getEmployee(){
return this.employee;
}
public void setEmployee(Employee e){
this.employee=e;
}
}
I could have used Spring annotations to configure it as a Spring Component, but we will use XML based configuration in this project. EmployeeService class is very standard and just provides us an access point for Employee beans.
Конфигурация Spring Bean с AOP
Если вы используете STS, у вас есть возможность создать “Файл Конфигурации Spring Bean” и выбрать пространство имен схемы AOP, но если вы используете другую IDE, вы можете просто добавить его в файл конфигурации spring bean. Мой файл конфигурации проекта выглядит следующим образом. spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="https://www.springframework.org/schema/aop"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-4.0.xsd
https://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!-- Enable AspectJ style of Spring AOP -->
<aop:aspectj-autoproxy />
<!-- Configure Employee Bean and initialize it -->
<bean name="employee" class="com.journaldev.spring.model.Employee">
<property name="name" value="Dummy Name"></property>
</bean>
<!-- Configure EmployeeService bean -->
<bean name="employeeService" class="com.journaldev.spring.service.EmployeeService">
<property name="employee" ref="employee"></property>
</bean>
<!-- Configure Aspect Beans, without this Aspects advices wont execute -->
<bean name="employeeAspect" class="com.journaldev.spring.aspect.EmployeeAspect" />
<bean name="employeeAspectPointcut" class="com.journaldev.spring.aspect.EmployeeAspectPointcut" />
<bean name="employeeAspectJoinPoint" class="com.journaldev.spring.aspect.EmployeeAspectJoinPoint" />
<bean name="employeeAfterAspect" class="com.journaldev.spring.aspect.EmployeeAfterAspect" />
<bean name="employeeAroundAspect" class="com.journaldev.spring.aspect.EmployeeAroundAspect" />
<bean name="employeeAnnotationAspect" class="com.journaldev.spring.aspect.EmployeeAnnotationAspect" />
</beans>
Для использования Spring AOP в Spring бинах, нам нужно сделать следующее:
- Объявить пространство имен AOP как xmlns:aop=“https://www.springframework.org/schema/aop”
- Добавить элемент aop:aspectj-autoproxy для включения поддержки Spring AspectJ с автоматическим проксированием во время выполнения
- Настроить классы Aspect как другие Spring бины
Вы можете видеть, что у меня много аспектов, определенных в файле конфигурации бина Spring, пора рассмотреть их по очереди.
Пример аспекта Spring AOP Before
Код EmployeeAspect.java:
package com.journaldev.spring.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class EmployeeAspect {
@Before("execution(public String getName())")
public void getNameAdvice(){
System.out.println("Executing Advice on getName()");
}
@Before("execution(* com.journaldev.spring.service.*.get*())")
public void getAllAdvice(){
System.out.println("Service method getter called");
}
}
Важные моменты в вышеуказанном классе аспекта:
- Классы аспектов должны иметь аннотацию
@Aspect
. - Аннотация @Before используется для создания совета Before.
- Строковый параметр, переданный в аннотацию
@Before
, является выражением Pointcut. - Совет getNameAdvice() будет выполняться для любого метода Spring Bean с сигнатурой
public String getName()
. Это очень важно запомнить, если мы создадим бин Employee с помощью оператора new, советы не будут применяться. Только когда мы будем использовать ApplicationContext для получения бина, советы будут применены. - Мы можем использовать звездочку (*) как подстановочный знак в выражениях Pointcut, getAllAdvice() будет применен ко всем классам в пакете
com.journaldev.spring.service
, имена которых начинаются сget
и не принимают аргументов.
Мы рассмотрим работу советов в действии в тестовом классе после того, как рассмотрим все различные типы советов.
Spring AOP Методы точек врезки и их повторное использование
Иногда нам приходится использовать одно и то же выражение точки врезки в нескольких местах. Мы можем создать пустой метод с аннотацией @Pointcut
и затем использовать его как выражение в советах. Код EmployeeAspectPointcut.java:
package com.journaldev.spring.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class EmployeeAspectPointcut {
@Before("getNamePointcut()")
public void loggingAdvice(){
System.out.println("Executing loggingAdvice on getName()");
}
@Before("getNamePointcut()")
public void secondAdvice(){
System.out.println("Executing secondAdvice on getName()");
}
@Pointcut("execution(public String getName())")
public void getNamePointcut(){}
@Before("allMethodsPointcut()")
public void allServiceMethodsAdvice(){
System.out.println("Before executing service method");
}
//Точка врезки для выполнения на всех методах классов в пакете
@Pointcut("within(com.journaldev.spring.service.*)")
public void allMethodsPointcut(){}
}
В приведенном выше примере очевидно, что вместо выражения мы используем имя метода в аргументе аннотации совета.
Spring AOP JoinPoint и аргументы советов
Мы можем использовать JoinPoint в качестве параметра в методах советов и, используя его, получить сигнатуру метода или целевой объект. Мы можем использовать выражение args()
в точке врезки, чтобы оно применялось к любому методу, соответствующему образцу аргумента. Если мы используем это, то мы должны использовать то же самое имя в методе совета, откуда определяется тип аргумента. Мы также можем использовать Обобщенные объекты в аргументах советов. Код EmployeeAspectJoinPoint.java:
package com.journaldev.spring.aspect;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class EmployeeAspectJoinPoint {
@Before("execution(public void com.journaldev.spring.model..set*(*))")
public void loggingAdvice(JoinPoint joinPoint){
System.out.println("Before running loggingAdvice on method="+joinPoint.toString());
System.out.println("Agruments Passed=" + Arrays.toString(joinPoint.getArgs()));
}
//Аргументы советов будут применены к методам бина с одним аргументом типа String
@Before("args(name)")
public void logStringArguments(String name){
System.out.println("String argument passed="+name);
}
}
Пример Spring AOP After Advice
Давайте рассмотрим простой класс аспекта с примером советов After, After Throwing и After Returning. Код EmployeeAfterAspect.java:
package com.journaldev.spring.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class EmployeeAfterAspect {
@After("args(name)")
public void logStringArguments(String name){
System.out.println("Running After Advice. String argument passed="+name);
}
@AfterThrowing("within(com.journaldev.spring.model.Employee)")
public void logExceptions(JoinPoint joinPoint){
System.out.println("Exception thrown in Employee Method="+joinPoint.toString());
}
@AfterReturning(pointcut="execution(* getName())", returning="returnString")
public void getNameReturningAdvice(String returnString){
System.out.println("getNameReturningAdvice executed. Returned String="+returnString);
}
}
Мы можем использовать within
в выражении точки среза, чтобы применить совет ко всем методам в классе. Мы можем использовать совет @AfterReturning, чтобы получить объект, возвращаемый методом, к которому применен совет. У нас есть метод throwException() в бине Employee для демонстрации использования совета After Throwing.
Пример Spring AOP Around Aspect
Как объяснено ранее, мы можем использовать аспект Around, чтобы перехватывать выполнение метода до и после. Мы можем использовать его для контроля того, будет ли выполняться адаптированный метод или нет. Мы также можем проверить возвращаемое значение и изменить его. Это самый мощный совет и его нужно правильно применять. Код EmployeeAroundAspect.java:
package com.journaldev.spring.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class EmployeeAroundAspect {
@Around("execution(* com.journaldev.spring.model.Employee.getName())")
public Object employeeAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
System.out.println("Before invoking getName() method");
Object value = null;
try {
value = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("After invoking getName() method. Return value="+value);
return value;
}
}
Вокруг советов всегда требуется иметь ProceedingJoinPoint в качестве аргумента, и мы должны использовать его метод proceed(), чтобы вызвать метод целевого объекта, советуемый метод. Если советуемый метод что-то возвращает, ответственность за это возлагается на совет. Для методов с возвращаемым типом void метод совета может вернуть null. Поскольку вокруг совета обрезает советуемый метод, мы можем контролировать ввод и вывод метода, а также его поведение при выполнении.
Совет Spring с пользовательским точечным срезом
Если вы посмотрите на все вышеуказанные выражения точек среза советов, есть шансы, что они могут применяться к другим бинам, где это не предполагалось. Например, кто-то может определить новый бин Spring с методом getName(), и совет начнет применяться к нему, даже если это не предполагалось. Вот почему мы должны держать область выражения точки среза как можно уже. Альтернативный подход – создать пользовательскую аннотацию и аннотировать методы, к которым мы хотим применить совет. Для этого служит метод Employee setName(), помеченный аннотацией @Loggable. Аннотация Spring Framework @Transactional отличный пример такого подхода для Управления транзакциями Spring. Код Loggable.java:
package com.journaldev.spring.aspect;
public @interface Loggable {
}
Код EmployeeAnnotationAspect.java:
package com.journaldev.spring.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class EmployeeAnnotationAspect {
@Before("@annotation(com.journaldev.spring.aspect.Loggable)")
public void myAdvice(){
System.out.println("Executing myAdvice!!");
}
}
Метод myAdvice() будет советовать только метод setName(). Это очень безопасный подход, и когда мы хотим применить совет к какому-либо методу, все, что нам нужно, – это пометить его аннотацией Loggable.
Конфигурация Spring AOP XML
I always prefer annotation but we also have the option to configure aspects in the spring configuration file. For example, let’s say we have a class as below. EmployeeXMLConfigAspect.java code:
package com.journaldev.spring.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
public class EmployeeXMLConfigAspect {
public Object employeeAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
System.out.println("EmployeeXMLConfigAspect:: Before invoking getName() method");
Object value = null;
try {
value = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("EmployeeXMLConfigAspect:: After invoking getName() method. Return value="+value);
return value;
}
}
Мы можем настроить это, включив следующую конфигурацию в файл конфигурации Spring Bean.
<bean name="employeeXMLConfigAspect" class="com.journaldev.spring.aspect.EmployeeXMLConfigAspect" />
<!-- Spring AOP XML Configuration -->
<aop:config>
<aop:aspect ref="employeeXMLConfigAspect" id="employeeXMLConfigAspectID" order="1">
<aop:pointcut expression="execution(* com.journaldev.spring.model.Employee.getName())" id="getNamePointcut"/>
<aop:around method="employeeAroundAdvice" pointcut-ref="getNamePointcut" arg-names="proceedingJoinPoint"/>
</aop:aspect>
</aop:config>
Элементы конфигурации AOP xml ясны по своему названию, поэтому я не буду вдаваться в подробности.
Пример Spring AOP
Давайте возьмем простую программу Spring и посмотрим, как все эти аспекты проникают через методы бина. Код SpringMain.java:
package com.journaldev.spring.main;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.journaldev.spring.service.EmployeeService;
public class SpringMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
EmployeeService employeeService = ctx.getBean("employeeService", EmployeeService.class);
System.out.println(employeeService.getEmployee().getName());
employeeService.getEmployee().setName("Pankaj");
employeeService.getEmployee().throwException();
ctx.close();
}
}
Теперь, когда мы выполняем указанную выше программу, мы получаем следующий вывод.
Mar 20, 2014 8:50:09 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4b9af9a9: startup date [Thu Mar 20 20:50:09 PDT 2014]; root of context hierarchy
Mar 20, 2014 8:50:09 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring.xml]
Service method getter called
Before executing service method
EmployeeXMLConfigAspect:: Before invoking getName() method
Executing Advice on getName()
Executing loggingAdvice on getName()
Executing secondAdvice on getName()
Before invoking getName() method
After invoking getName() method. Return value=Dummy Name
getNameReturningAdvice executed. Returned String=Dummy Name
EmployeeXMLConfigAspect:: After invoking getName() method. Return value=Dummy Name
Dummy Name
Service method getter called
Before executing service method
String argument passed=Pankaj
Before running loggingAdvice on method=execution(void com.journaldev.spring.model.Employee.setName(String))
Agruments Passed=[Pankaj]
Executing myAdvice!!
Running After Advice. String argument passed=Pankaj
Service method getter called
Before executing service method
Exception thrown in Employee Method=execution(void com.journaldev.spring.model.Employee.throwException())
Exception in thread "main" java.lang.RuntimeException: Dummy Exception
at com.journaldev.spring.model.Employee.throwException(Employee.java:19)
at com.journaldev.spring.model.Employee$$FastClassBySpringCGLIB$$da2dc051.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:711)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:58)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
at com.journaldev.spring.model.Employee$$EnhancerBySpringCGLIB$$3f881964.throwException(<generated>)
at com.journaldev.spring.main.SpringMain.main(SpringMain.java:17)
Вы можете видеть, что советы выполняются один за другим в соответствии с их конфигурациями точек врезки. Вы должны настраивать их по одному, чтобы избежать путаницы. Вот и все для Учебного пособия по примеру Spring AOP, я надеюсь, вы усвоили основы AOP с Spring и сможете узнать больше из примеров. Загрузите образец проекта по ссылке ниже и поиграйтесь с ним.