Статьи

JSON для полиморфной сериализации Java-объектов

В течение долгого времени JSON является стандартом де-факто для всех видов сериализации данных между клиентом и сервером. Среди прочего, его сильные стороны — простота и удобочитаемость. Но с простотой приходят некоторые ограничения, одно из которых я бы хотел сегодня обсудить: хранение и извлечение полиморфных объектов Java.

Начнем с простой проблемы: иерархия фильтров. Существует один абстрактный класс AbstractFilter и два подкласса, RegexFilter и StringMatchFilter .

1
2
3
4
5
package bean.json.examples;
 
public abstract class AbstractFilter {
    public abstract void filter();
}

Вот класс RegexFilter :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package bean.json.examples;
 
public class RegexFilter extends AbstractFilter {
    private String pattern;
 
    public RegexFilter( final String pattern ) {
        this.pattern = pattern;
    }
 
    public void setPattern( final String pattern ) {
        this.pattern = pattern;
    }
 
    public String getPattern() {
        return pattern;
    }
 
    @Override
    public void filter() {
        // Do some work here
    }
}

А вот класс StringMatchFilter :

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
package bean.json.examples;
 
public class StringMatchFilter extends AbstractFilter {
    private String[] matches;
    private boolean caseInsensitive;
 
    public StringMatchFilter() {
    }
 
    public StringMatchFilter( final String[] matches, final boolean caseInsensitive ) {
        this.matches = matches;
        this.caseInsensitive = caseInsensitive;
    }
 
    public String[] getMatches() {
        return matches;
    }
 
    public void setCaseInsensitive( final boolean caseInsensitive ) {
        this.caseInsensitive = caseInsensitive;
    }
 
    public void setMatches( final String[] matches ) {
        this.matches = matches;
    }
 
    public boolean isCaseInsensitive() {
        return caseInsensitive;
    }
 
    @Override
    public void filter() {
        // Do some work here
    }
}

Ничего особенного, чистые бобы Java. А что если нам нужно сохранить список экземпляров AbstractFilter в JSON и, что более важно, восстановить этот список обратно из JSON? Следующий класс Filters демонстрирует, что я имею в виду:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package bean.json.examples;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
 
public class Filters {
    private Collection< AbstractFilter > filters = new ArrayList< AbstractFilter >();
 
    public Filters() {
    }
 
    public Filters( final AbstractFilter ... filters ) {
        this.filters.addAll( Arrays.asList( filters ) );
    }
 
    public Collection< AbstractFilter > getFilters() {
        return filters;
    }
 
    public void setFilters( final Collection< AbstractFilter > filters ) {
        this.filters = filters;
    }
}

Поскольку JSON является текстовым, независимым от платформы форматом, он не несет никакой специфической для типа информации. Благодаря потрясающему процессору JSON от Jackson это можно легко сделать. Итак, давайте добавим JSON-процессор Jackson в наш файл POM:

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
<project>
    
 <modelversion>
  4.0.0
 </modelversion>
 
    
 <groupid>
  bean.json
 </groupid>
    
 <artifactid>
  examples
 </artifactid>
    
 <version>
  0.0.1-SNAPSHOT
 </version>
    
 <packaging>
  jar
 </packaging>
 
    
 <properties>
        
  <project.build.sourceencoding>
   UTF-8
  </project.build.sourceencoding>
    
 </properties>
 
    
 <dependencies>
        
  <dependency>
            
   <groupid>
    org.codehaus.jackson
   </groupid>
            
   <artifactid>
    jackson-mapper-asl
   </artifactid>
            
   <version>
    1.9.6
   </version>
        
  </dependency>
    
 </dependencies>
 
</project>

Выполнив этот шаг, нам нужно сообщить Джексону, что мы намерены хранить информацию о типе вместе с нашими объектами в JSON, чтобы впоследствии можно было восстановить точные объекты из JSON. Несколько аннотаций на AbstractFilter делают именно это.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
import org.codehaus.jackson.annotate.JsonSubTypes;
import org.codehaus.jackson.annotate.JsonSubTypes.Type;
import org.codehaus.jackson.annotate.JsonTypeInfo;
import org.codehaus.jackson.annotate.JsonTypeInfo.Id;
 
@JsonTypeInfo( use = Id.NAME )
@JsonSubTypes(
    {
        @Type( name = "Regex", value = RegexFilter.class ),
        @Type( name = "StringMatch", value = StringMatchFilter.class )
    }
)
public abstract class AbstractFilter {
    // ...
}

Вот и все! Следующий вспомогательный класс выполняет грязную работу по сериализации фильтров в строку и десериализации их обратно из строки, используя ObjectMapper Джексона :

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
package bean.json.examples;
 
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
 
import org.codehaus.jackson.map.ObjectMapper;
 
public class FilterSerializer {
    private final ObjectMapper mapper = new ObjectMapper();
 
    public String serialize( final Filters filters ) {
        final StringWriter writer = new StringWriter();
        try {
            mapper.writeValue( writer, filters );
            return writer.toString();
        } catch( final IOException ex ) {
            throw new RuntimeException( ex.getMessage(), ex );
        } finally {
            try { writer.close(); } catch ( final IOException ex ) { /* Nothing to do here */ }
        }
    }
 
    public Filters deserialize( final String str ) {
        final StringReader reader = new StringReader( str );
        try {
            return mapper.readValue( reader, Filters.class );
        } catch( final IOException ex ) {
            throw new RuntimeException( ex.getMessage(), ex );
        } finally {
            reader.close();
        }
    }
}

Давайте посмотрим это в действии. Следующий пример кода

1
2
3
4
5
6
final String json = new FilterSerializer().serialize(
    new Filters(
        new RegexFilter( "\\d+" ),
        new StringMatchFilter( new String[] { "String1", "String2" }, true )
    )
);

производит следующий JSON:

1
2
3
4
5
6
{ "filters":
  [
     {"@type":"Regex","pattern":"\\d+"},
     {"@type":"StringMatch","matches":["String1","String2"],"caseInsensitive":true}
  ]
}

Как видите, каждая запись в коллекции «filters» имеет свойство «@type», которое имеет значение, которое мы указали с помощью аннотирования класса AbstractFilter . Вызов нового FilterSerializer (). Deserialize (json) создает точно такой же экземпляр объекта Filters .

Ссылка: JSON для полиморфной сериализации Java-объектов от нашего партнера по JCG Андрея Редько в блоге Андрея Редько .