Статьи

Преобразование типов весной

Вот несколько случаев, когда нам нужно преобразование типов:
Дело 1. Чтобы упростить настройку bean-компонентов, Spring поддерживает преобразование значений свойств в текстовые значения и из них. Каждый редактор свойств предназначен только для свойства определенного типа. И чтобы использовать их, мы должны зарегистрировать их в контейнере Spring.
Дело № 2. Также при использовании Spring MVC контроллеры привязывают значения поля формы к свойствам объекта. Предположим, что объект состоит из другого объекта, тогда контроллер MVC не может автоматически присваивать значения внутреннему объекту пользовательского типа, поскольку все значения в форме вводятся как текстовые значения. Контейнер Spring принимает преобразование текстовых значений в примитивные типы, но не в объекты пользовательских типов. Для этого нужно инициализировать пользовательские редакторы в потоке MVC.

В этой статье будут обсуждаться различные способы реализации преобразователей для объектов пользовательских типов. Чтобы подробнее остановиться на них, рассмотрим следующий вариант использования. В этом примере я хотел бы смоделировать систему бронирования игрового поля. Итак, вот мои доменные объекты:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Reservation {
 
    public String playGround;
    public Date dateToReserve;
    public int hour;
    public Player captain;
    public SportType sportType;
 
    public Reservation(String playGround, Date date, int hour, Player captain, SportType sportType) {
        this.playGround = playGround;
        this.dateToReserve = date;
        this.hour = hour;
        this.captain = captain;
        this.sportType = sportType;
    }
 
    /**
     * Getters and Setters
     */
}
 
public class Player {
 
    private String name;
    private String phone;
    /**
     * Getters and Setters
     */
 
}
 
public class SportType {
 
    public static final SportType TENNIS = new SportType(1, "Tennis");
    public static final SportType SOCCER = new SportType(2, "Soccer");
 
    private int id;
    private String name;
 
    public SportType(int id, String name) {
        this.id = id;
        this.name = name;
    }
 
    public static Iterable<SportType> list(){
        return Arrays.asList(TENNIS, SOCCER);
    }
 
    public static SportType getSport(int id){
        for(SportType sportType : list()){
            if(sportType.getId() == id){
                return sportType;
            }
        }
        return null;
    }
 
    /**
     * Getters and Setters
     */
}

Вот пример случая № 1: Предположим, что мы хотим определить компонент резервирования, вот как мы это делаем:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<bean id="dummyReservation" class="com.pramati.model.Reservation">
    <property name="playGround" value="Soccer Court #1"/>
    <property name="dateToReserve" value="11-11-2011"/>
    <property name="hour" value="15"/>
    <property name="captain">
        <bean class="com.pramati.model.Player">
            <property name="name" value="Prasanth"/>
            <property name="phone" value="92131233124"/>
        </bean>
    </property>
    <property name="sportType">
        <property name="id" value="1"/>
        <property name="name" value="TENNIS"/>
    </property>
</bean>

Это определение бина довольно многословно. Это могло бы быть более презентабельно, если определение выглядит примерно так:

1
2
3
4
5
6
7
<bean id="dummyReservation" class="com.pramati.model.Reservation">
    <property name="playGround" value="Soccer Court #1"/>
    <property name="dateToReserve" value="11-11-2011"/>
    <property name="hour" value="15"/>
    <property name="captain" value="Prasanth,92131233124"/>
    <property name="sportType" value="1,TENNIS"/>
</bean>

Чтобы это работало, мы должны указать Spring использовать пользовательские конвертеры в процессе определения bean-компонента.

Вот пример случая №2. Предположим, у меня в приложении есть JSP, который позволяет пользователю зарезервировать игровую площадку на определенное время дня.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Reservation Form</title>
<style type="text/css">
.error {
    color: #ff0000;
    font-weight: bold;
}
</style>
</head>
<body>
    <form:form method="post" modelAttribute="reservation">
        <table>
            <tr>
                <th>Court Name</th>
                <td><form:input path="courtName" /></td>
            </tr>
            <tr>
                <th>Reservation Date</th>
                <td><form:input path="date" /></td>
            </tr>
            <tr>
                <th>Hour</th>
                <td><form:input path="hour" /></td>
            </tr>
            <tr>
                <th>Player Name</th>
                <td><form:input path="player.name" /></td>
            </tr>
            <tr>
                <th>Player Contact Number</th>
                <td><form:input path="player.phone" /></td>
            </tr>
            <tr>
                <th>Sport Type</th>
                <td><form:select path="sportType" items="${sportTypes}"
                        itemLabel="name" itemValue="id" /></td>
            </tr>
            <tr>
                <td colspan="3"><input type="submit" name="Submit" /></td>
            </tr>
        </table>
    </form:form>
</body>
</html>

А вот соответствующий контроллер MVC:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Controller
@RequestMapping
@SessionAttributes("reservation")
public class ReservationFormController {
 
    @Autowired
    private ReservationService reservationService;
 
    @ModelAttribute("sportTypes")
    public Iterable<SportType> getSportTypes(){
        return SportType.list();
    }
 
    @RequestMapping(value="/reservationForm/{captainName}", method=RequestMethod.GET)
    public String initForm(Model model, @PathVariable String captainName){
        Reservation reservation = new Reservation();
        reservation.setPlayer(new Player(captainName, null));
        reservation.setSportType(SportType.TENNIS);
        model.addAttribute("reservation", reservation);
        return "reservationForm";
    }
 
    @RequestMapping(value="/reservationForm/{captainName}",method=RequestMethod.POST)
    public String reserve(@Valid Reservation reservation, BindingResult bindingResult, SessionStatus sessionStatus){
        validator.validate(reservation, bindingResult);
        if(bindingResult.hasErrors()){
            return "/reservationForm";
        } else{
            reservationService.make(reservation);
            sessionStatus.setComplete();
            return "redirect:../reservationSuccess";
        }
    }
}

Теперь, как вы видите, в JSP мы связываем поля формы с объектом Reservation (modelAttribute = ”booking”). Этот объект хранится в модели контроллером (в методе initForm ()), который передается в представление. Теперь, когда мы отправляем форму, Spring выдает сообщение проверки о том, что значения полей не могут быть преобразованы в типы Player и SportType. Чтобы это работало, мы должны определить пользовательские конвертеры и добавить их в поток Spring MVC.

Теперь вопрос, как определить пользовательские конвертеры? Spring предоставляет два способа поддержки этих пользовательских конвертеров:

  • Решение № 1: Используйте PropertyEditors
  • Решение № 2: Используйте конвертеры

Используя PropertyEditor:

PropertyEditorSupport, реализующий интерфейс PropertyEditor, является классом поддержки, помогающим создавать PropertyEditors.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SportTypeEditorSupport extends PropertyEditorSupport {
 
    /**
     * Sets the property value by parsing a given String.  May raise
     * java.lang.IllegalArgumentException if either the String is
     * badly formatted or if this kind of property can't be expressed
     * as text.
     *
     * @param text  The string to be parsed.
     */
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        try{
            SportType sportType = SportType.getSport(Integer.parseInt(text));
            setValue(sportType);// setValue stores the custom type Object into a instance variable in PropertyEditorSupport.
        }
        catch(NumberFormatException nfe){
            throw new RuntimeException(nfe.getMessage());
        }
    }
 
     /**
     * Gets the property value as a string suitable for presentation
     * to a human to edit.
     *
     * @return The property value as a string suitable for presentation
     *       to a human to edit.
     * <p>   Returns "null" is the value can't be expressed as a string.
     * <p>   If a non-null value is returned, then the PropertyEditor should
     *       be prepared to parse that string back in setAsText().
     */
    @Override
    public String getAsText() {
        SportType sportType = (SportType)getValue();
        return Integer.toString(sportType.getId());
    }
}

Теперь зарегистрируйте пользовательский редактор в PropertyEditorRegistry. PropertyEditorRegistrar регистрирует пользовательские редакторы в PropertyEditorRegistry. Вот как вы это делаете:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import java.text.SimpleDateFormat;
import java.util.Date;
 
import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.propertyeditors.CustomDateEditor;
 
import com.pramati.model.SportType;
 
public class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
    @Override
    public void registerCustomEditors(PropertyEditorRegistry registry) {
        registry.registerCustomEditor(Date.class, new CustomDateEditor(
                new SimpleDateFormat("dd-MM-yyyy"), true));
        registry.registerCustomEditor(SportType.class, new SportTypeEditorSupport());
    }
}

CustomEditorConfigurer реализован в виде постпроцессора фабричного компонента, чтобы вы могли зарегистрировать свои редакторы пользовательских свойств до того, как будет создан какой-либо из компонентов. Для этого мы связываем PropertyEditorRegistry с CustomEditorConfigurer.

1
2
3
4
5
6
7
8
9
<bean id="customPropertyEditorRegistrar" class="com.pramati.spring.mvc.CustomPropertyEditorRegistrar"/>
 
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>

Теперь, когда Spring контейнер видит это:

1
<property name="captain" value="Prasanth,92131233124"/>

Spring автоматически преобразует значение, указанное в объект Player. Но этой конфигурации недостаточно для Spring MVC потока. Контроллеры все равно будут жаловаться на несовместимые типы, поскольку он ожидает объект Player, где он получает String. Чтобы значение поля формы интерпретировалось как объект нестандартного типа, нам нужно сделать несколько изменений конфигурации MVC.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.WebRequest;
 
public class CustomWebBindingInitializer implements WebBindingInitializer {
    @Autowired
    private CustomPropertyEditorRegistrar customPropertyEditorRegistrar;
 
    @Override
    public void initBinder(WebDataBinder binder, WebRequest request) {
        customPropertyEditorRegistrar.registerCustomEditors(binder);
    }
}

Теперь удалите и определите необходимые bean-компоненты вручную, поскольку нам нужно внедрить WebBindingInitializer в RequestMappingHandlerAdapter.

1
2
3
4
5
6
7
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
 
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="webBindingInitializer">
        <bean class="com.pramati.spring.mvc.CustomWebBindingInitializer"/>
    </property>
</bean>

Теперь контроллер автоматически преобразует String в необходимый объект пользовательского типа. Обратите внимание, что мы должны сделать отдельные изменения конфигурации для упрощения конфигурации bean-компонентов и преобразования типов полей формы в Spring MVC. Также стоит отметить, что при расширении PropertyEditorSupport мы сохраняем объект пользовательского типа в переменной экземпляра и, следовательно, использование PropertyEditors не является поточно-ориентированным. Чтобы преодолеть эти проблемы, Spring 3.0 представил концепцию конвертеров и форматеров.

Использование конвертеров:

Компоненты преобразователя используются для преобразования одного типа в другой тип, а также для обеспечения более чистого разделения путем принудительного размещения всего такого кода, связанного с преобразованием, в одном месте. Spring уже поддерживает встроенные конвертеры для часто используемых типов, и фреймворк достаточно расширяем для написания пользовательских конвертеров. Spring Formatters приходят в изображение, чтобы отформатировать данные в соответствии с отображением, на котором они отображаются. Всегда стоит посмотреть исчерпывающий список готовых конвертеров, прежде чем даже подумать о написании специального конвертера, подходящего для конкретной бизнес-задачи. Для просмотра списка готовых конвертеров, посмотрите пакет org.springframework.core.convert.support

Возвращаясь к нашему варианту использования, давайте реализуем конвертер String в SportType:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import com.pramati.model.SportType;
 
public class StringToSportTypeConverter implements Converter<String, SportType> {
 
    @Override
    public SportType convert(String sportIdStr) {
        int sportId = -1;
        try{
            sportId = Integer.parseInt(sportIdStr);
        } catch (NumberFormatException e) {
            throw new ConversionFailedException(TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(SportType.class), sportIdStr, null);
        }
 
        SportType sportType = SportType.getSport(sportId);
        return sportType;
    }
 
}

Теперь зарегистрируйте это в ConversionService и свяжите с потоком SpringMVC:

01
02
03
04
05
06
07
08
09
10
11
<mvc:annotation-driven conversion-service="conversionService"/>
 
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" >
    <property name="converters">
        <set>
            <bean class="com.pramati.type.converters.StringToSportTypeConverter"/>
            <bean class="com.pramati.type.converters.StringToDateConverter"/>
            <bean class="com.pramati.type.converters.StringToPlayerConverter"/>
        </set>
    </property>
</bean>

Если вы используете пользовательские объявления bean вместо ‹mvc: annotation-driven /›, вот способ сделать это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.WebRequest;
 
public class CustomWebBindingInitializer implements WebBindingInitializer {
 
    @Autowired
    private ConversionService conversionService;
 
    @Override
    public void initBinder(WebDataBinder binder, WebRequest request) {
        binder.setConversionService(conversionService);
    }
 
}

Теперь добавьте WebBindingInitializer в RequestMappingHandlerAdapter.

1
2
3
4
5
6
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="webBindingInitializer">
        <bean class="com.pramati.spring.mvc.CustomWebBindingInitializer"/>
    </property>
</bean>

Одна только служба ConversionService позаботится об упрощении конфигурации бина (вариант № 1). Чтобы дело № 2 работало, мы должны зарегистрировать ConversionService в потоке Spring MVC. И обратите внимание, что этот способ преобразования типов также является потокобезопасным.

Также вместо того, чтобы сделать Converters / PropertEditors доступными для всех контроллеров в приложении, мы можем включить их для каждого контроллера. Вот как ты это делаешь. Удалите указанную выше общую конфигурацию и введите @InitBinder в класс контроллера следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Controller
@RequestMapping
@SessionAttributes("reservation")
public class ReservationFormController {
 
    private ReservationService reservationService;
    private ReservationValidator validator;
 
    @Autowired
    public ReservationFormController(ReservationService reservationService, ReservationValidator validator){
        this.reservationService = reservationService;
        this.validator = validator;
    }
 
    @Autowired
    private ConversionService conversionService;
    @InitBinder
    protected void initBinder(ServletRequestDataBinder binder) {
        binder.setConversionService(conversionService);
    }
 
    /*@InitBinder
    protected void initBinder(ServletRequestDataBinder binder) {
        binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("dd-MM-yyyy"), true));
        binder.registerCustomEditor(SportType.class, new SportTypeEditorSupport(reservationService));
    }*/
 
    /*@Autowired
    private PropertyEditorRegistrar propertyEditorRegistrar;
    @InitBinder
    protected void initBinder(ServletRequestDataBinder binder) {
        propertyEditorRegistrar.registerCustomEditors(binder);
    }*/
 
    @ModelAttribute("sportTypes")
    public Iterable<SportType> getSportTypes(){
        return SportType.list();
    }
 
    @RequestMapping(value="/reservationForm/{userName}", method=RequestMethod.GET)
    public String initForm(Model model, @PathVariable String userName){
        Reservation reservation = new Reservation();
        reservation.setPlayer(new Player(userName, null));
        reservation.setSportType(SportType.TENNIS);
        model.addAttribute("reservation", reservation);
        return "reservationForm";
    }
 
    @RequestMapping(value="/reservationForm/{userName}",method=RequestMethod.POST)
    public String reserve(@Valid Reservation reservation, BindingResult bindingResult, SessionStatus sessionStatus){
        validator.validate(reservation, bindingResult);
        if(bindingResult.hasErrors()){
            return "/reservationForm";
        } else{
            reservationService.make(reservation);
            sessionStatus.setComplete();
            return "redirect:../reservationSuccess";
        }
    }
 
    @RequestMapping("/reservationSuccess")
    public void success(){
 
    }
}

Поэтому, если вы видите приведенный выше код, вы бы заметили закомментированный код, в котором мы использовали PropertyEditors вместо конвертеров. И, следовательно, эта функция включения преобразователей типов на основе контроллера доступна в обеих реализациях.