Статьи

Представляем образец делегата

  • Делегат: лицо, которое избрано или избрано для голосования или выступает за других — Merriam-Webster .
  • Шаблон делегирования. В программной инженерии шаблон делегирования — это шаблон проектирования в объектно-ориентированном программировании, где объект вместо выполнения одной из заявленных задач делегирует эту задачу связанному вспомогательному объекту — Википедии .
  • Сделайте вещи как можно проще, но не проще — перефразируя Альберта Эйнштейна .

Spring Batch является важным инструментом в наборе инструментов Enterprise Java. Он обеспечивает отличную функциональность из коробки, особенно для чтения и записи данных из разных источников. В этом блоге мы представили несколько статей, представляющих Spring Batch. Если вы не знакомы с Spring Batch и тачлетом Reader, Processor, Writer, пожалуйста, найдите время и просмотрите их.

Перефразированная цитата, которую я использую выше, важна для меня. Одна из вещей, которые я пытаюсь сделать, — это сохранять код, который я поставляю, максимально поддерживаемым. Я хочу, чтобы он работал и работал хорошо, но код, который я проверяю сегодня, будет поддерживаться кем-то в будущем. Сохранение кода как можно более простым является одним из способов обеспечения легкого сопровождения кода.

Так что же происходит, когда у вас сложный источник данных, который вы должны обработать?

Мы обнаруживаем, что часто входные файлы, которые мы должны обрабатывать, не так просты, как одна запись на строку. Часто в файле несколько строк, описывающих только одну запись.

Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
HKaren Traviss
LAB00KW3VG2G
LI0345478274
LI0345511131
F00000003
HJim Butcher
LI0451457811
F00000001
HDave Duncan
LI0380791277
LI0345352912
F00000002
HRik Scarborough
LI9999999999
F00000001

Здесь у нас есть файл, который содержит четыре записи через пятнадцать строк. Каждая запись начинается со строки заголовка, содержит одну или несколько строк текста и заканчивается нижним колонтитулом. Заголовок содержит тип строки (H для заголовка) и имя. Строка также содержит тип строки (L), тип поиска, в данном примере код ISBN или Amazon и ключ для поиска книги. Нижний колонтитул, опять же, содержит тип линии и количество записей в этом блоке.

Используя стандартный Reader, каждая строка будет прочитана, а затем передана Процессору, который затем должен будет определить, с какой строкой он имеет дело. Затем обработчик должен будет сохранять информацию от каждого заголовка при обработке каждой строки тела, пока не будет обработан нижний колонтитул. Затем Writer должен знать о каждой строке, отправленной Процессором, и о том, должна ли она быть записана. Это сложно, отчасти потому, что несколько объектов должны знать о том, как читается файл, вместо того, чтобы Процессор заботился только об одном объекте, а Writer занимался только написанием того, что ему было дано.

Вместо этого, давайте представим шаблон Delegate в Reader и позволим ему обрабатывать создание всей записи. Поскольку у нас есть информация из нескольких строк, а также верхний и нижний колонтитулы, которые мы будем использовать для создания каждой записи, нам придется передать обработчику список записей. Наблюдатель среди вас заметит, что каждая запись содержит книжную нотацию ISBN или Amazon и может использоваться для поиска автора, который также содержится в заголовке. В примере из реальной жизни такой тип избыточности может и не может произойти.

Давайте обернем вывод в другой объект, чтобы было легче работать с ним.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
public class OrderReaderStep implements ItemReader<OrderList> {
 
    private static final Logger logger = LoggerFactory.getLogger(OrderReaderStep.class);
    private FlatFileItemReader
<FieldSet> delegate;
    private static final String FOOTER = "F*";
    private static final String BODY = "L*";
    private static final String HEADER = "H*";
 
    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        delegate = new FlatFileItemReader<>();
 
        delegate.setResource(new ClassPathResource("orders.txt"));
 
        final DefaultLineMapper
<FieldSet> defaultLineMapper = new DefaultLineMapper<>();
        final PatternMatchingCompositeLineTokenizer orderFileTokenizer = new PatternMatchingCompositeLineTokenizer();
        final Map<String, LineTokenizer> tokenizers = new HashMap<>();
        tokenizers.put(HEADER, buildHeaderTokenizer());
        tokenizers.put(BODY, buildBodyTokenizer());
        tokenizers.put(FOOTER, buildFooterTokenizer());
        orderFileTokenizer.setTokenizers(tokenizers);
        defaultLineMapper.setLineTokenizer(orderFileTokenizer);
        defaultLineMapper.setFieldSetMapper(new PassThroughFieldSetMapper());
 
        delegate.setLineMapper(defaultLineMapper);
 
        delegate.open(stepExecution.getExecutionContext());
    }
 
    @AfterStep
    public void afterStep(StepExecution stepExecution) {
        delegate.close();
    }
 
    @Override
    public OrderList read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        logger.info("start read");
 
        OrderList record = null;
 
        FieldSet line;
        List<Order> bodyList = new ArrayList<>();
        while ((line = delegate.read()) != null) {
            String prefix = line.readString("lineType");
            if (prefix.equals("H")) {
                record = new OrderList();
                record.setName(line.readString("name"));
            } else if (prefix.equals("L")) {
                Order order = new Order();
                order.setLookup(line.readString("lookupKey"));
                order.setLookupType(line.readString("keyType"));
                bodyList.add(order);
            } else if (prefix.equals("F")) {
                if (record != null) {
                    if (line.readLong("count") != bodyList.size()) {
                        throw new ValidationException("Size does not match file count");
                    }
                    record.setOrders(bodyList);
                }
                break;
            }
 
        }
        logger.info("end read");
        return record;
    }
 
    private LineTokenizer buildBodyTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
 
        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 2), // keyType
            new Range(3, 12) // lookup key
        });
 
        tokenizer.setNames(new String[]{ //
            "lineType",
            "keyType",
            "lookupKey"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }
 
    private LineTokenizer buildFooterTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
 
        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 9) // count
        });
 
        tokenizer.setNames(new String[]{ //
            "lineType",
            "count"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }
 
    private LineTokenizer buildHeaderTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
 
        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 20), // name
        });
 
        tokenizer.setNames(new String[]{ //
            "lineType",
            "name"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }
 
}

Этот Reader реализует интерфейс ItemReader. Это дает нам метод чтения, который вызывается заданием до тех пор, пока он не возвратит ноль, или в случае ошибки не выдаст исключение. В нашем Reader мы объявляем другого Reader, это FlatFileItemReader. Это наш Делегат или Объект, который был выбран для выполнения функции для нас. Наш метод чтения будет зацикливаться на чтении делегата, пока не будет прочитан нижний колонтитул Затем он объединит всю запись в свою оболочку и передаст ее процессору.

Перед началом использования делегатский читатель должен быть открыт, а затем должен быть закрыт только после того, как это сделано. Я открываю его здесь в BeforeStep, так как мне нужно его инициализировать и настроить здесь. Я мог бы также реализовать содержащий ридер как ItemStreamReader и использовать методы open, close, а также update, которые нам дает Interface.

Возврат упрощенного объекта в Процессор позволяет нам значительно упростить Процессор:

01
02
03
04
05
06
07
08
09
10
@Override
public List<BookList> process(OrderList orderList) throws Exception {
    logger.info("process");
    List<BookList> books = new ArrayList<>();
    for (Order order : orderList.getOrders()) {
        BookList bl = doProcessing(orderList.getName(), order);
        books.add(bl);
    }
    return books;
}

Метод doProcessing может содержать бизнес-логику для этого задания и должен создавать допустимый объект BookList. Поскольку мы имеем дело с несколькими записями, процесс создаст несколько списков книг, которые могут быть возвращены и переданы в Writer. Я оставлю это вам, чтобы заполнить оставшуюся часть этого объекта, но это просто стандартный ItemProcessor. Процессору не нужно сохранять информацию о записи между вызовами, поэтому программист может сосредоточиться на бизнес-логике.

Наш Writer реализует ItemStreamWriter. Это дает нам больше методов, чем ItemWriter, но если вы предпочитаете использовать ItemWriter аналогично тому, как мы это делали в Reader, убедитесь, что вы открыли делегат в BeforeStep и закрыли его в AfterStep.

Использование делегата в Writer дает нам возможность тщательно изучить список, который Writer получает от Reader и Process.

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
public class ListWriter implements ItemStreamWriter<List<BookList>> {
 
    private static final Logger logger = LoggerFactory.getLogger(ListWriter.class);
 
    private FlatFileItemWriter<BookList> delegate;
 
    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        delegate = new FlatFileItemWriter<>();
        delegate.setResource(new FileSystemResource("booklist.csv"));
        delegate.setShouldDeleteIfEmpty(true);
        delegate.setAppendAllowed(true);
 
        DelimitedLineAggregator<BookList> dla = new DelimitedLineAggregator<>();
        dla.setDelimiter(",");
        BeanWrapperFieldExtractor<BookList> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[]{"bookName", "author"});
        dla.setFieldExtractor(fieldExtractor);
        delegate.setLineAggregator(dla);
    }
 
    @Override
    public void close() throws ItemStreamException {
        delegate.close();
    }
 
    @Override
    public void open(ExecutionContext ec) throws ItemStreamException {
        delegate.open(ec);
    }
 
    @Override
    public void update(ExecutionContext ec) throws ItemStreamException {
        delegate.update(ec);
    }
 
    @Override
    public void write(List<? extends List<BookList>> list) throws Exception {
        logger.info("write");
        for (List<BookList> bookList : list) {
            delegate.write(bookList);
        }
    }
 
}

Это дает нам следующий вывод:

1
2
3
4
5
6
7
Going Grey,Karen Traviss
Hard Contact,Karen Traviss
501st,Karen Traviss
Storm Front,Jim Butcher
Lord of the Fire Lands,Dave Duncan
The Reluctant Swordsman,Dave Duncan
Wolfbrander Series Unpublished,Rik Scarborough

Так что же происходит, если это немного сложнее, а входной файл не содержит нижний колонтитул?

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

01
02
03
04
05
06
07
08
09
10
11
HKaren Traviss
LAB00KW3VG2G
LI0345478274
LI0345511131
HJim Butcher
LI0451457811
HDave Duncan
LI0380791277
LI0345352912
HRik Scarborough
LI9999999999

Просить нашего нынешнего писателя о том, чтобы читать вперед и удерживать эту запись во время следующего вызова, излишне сложно, что приводит к головной боли при обслуживании. Тем не менее, мы можем упростить это с помощью PeekableItemReader:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
class OrderReaderStep2 implements ItemStreamReader<OrderList> {
 
    private static final String BODY = "L*";
    private static final String HEADER = "H*";
    private static final Logger logger = LoggerFactory.getLogger(OrderReaderStep2.class);
    private SingleItemPeekableItemReader
<FieldSet> delegate;
 
    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        FlatFileItemReader fileReader = new FlatFileItemReader<>();
 
        fileReader.setResource(new ClassPathResource("orders2.txt"));
 
        final DefaultLineMapper
<FieldSet> defaultLineMapper = new DefaultLineMapper<>();
        final PatternMatchingCompositeLineTokenizer orderFileTokenizer = new PatternMatchingCompositeLineTokenizer();
        final Map<String, LineTokenizer> tokenizers = new HashMap<>();
        tokenizers.put(HEADER, buildHeaderTokenizer());
        tokenizers.put(BODY, buildBodyTokenizer());
        orderFileTokenizer.setTokenizers(tokenizers);
        defaultLineMapper.setLineTokenizer(orderFileTokenizer);
        defaultLineMapper.setFieldSetMapper(new PassThroughFieldSetMapper());
 
        fileReader.setLineMapper(defaultLineMapper);
 
        delegate = new SingleItemPeekableItemReader<>();
        delegate.setDelegate(fileReader);
    }
 
    @Override
    public void close() throws ItemStreamException {
        delegate.close();
    }
 
    @Override
    public void open(ExecutionContext ec) throws ItemStreamException {
        delegate.open(ec);
    }
 
    @Override
    public OrderList read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        logger.info("start read");
 
        OrderList record = null;
 
        FieldSet line;
        List<Order> bodyList = new ArrayList<>();
        while ((line = delegate.read()) != null) {
            String prefix = line.readString("lineType");
            if (prefix.equals("H")) {
                record = new OrderList();
                record.setName(line.readString("name"));
            } else if (prefix.equals("L")) {
                Order order = new Order();
                order.setLookup(line.readString("lookupKey"));
                order.setLookupType(line.readString("keyType"));
                bodyList.add(order);
            }
 
            FieldSet nextLine = delegate.peek();
            if (nextLine == null || nextLine.readString("lineType").equals("H")) {
                record.setOrders(bodyList);
                break;
            }
 
        }
        logger.info("end read");
        return record;
    }
 
    @Override
    public void update(ExecutionContext ec) throws ItemStreamException {
        delegate.update(ec);
    }
 
    private LineTokenizer buildBodyTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
 
        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 2), // keyType
            new Range(3, 12) // lookup key
        });
 
        tokenizer.setNames(new String[]{ //
            "lineType",
            "keyType",
            "lookupKey"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }
 
    private LineTokenizer buildHeaderTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
 
        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 20), // name
        });
 
        tokenizer.setNames(new String[]{ //
            "lineType",
            "name"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }
 
}

На этот раз я реализую содержащий Reader как ItemStreamReader, чтобы показать вам разницу. Это могло быть реализовано как ItemReader, как и предыдущий.

PeekableItemReader позволяет нам смотреть вперед к следующей записи, чтобы увидеть, достигли ли мы конца записи или конца файла. Затем один и тот же процессор и устройство записи могут использоваться для получения того же вывода, что и раньше.

Последние мысли

На первый взгляд шаблон делегата может показаться не таким простым, как использование одного устройства чтения или записи. Существует больше настроек для обоих этих объектов. Но моя любимая перефразированная цитата говорит, что она настолько проста, насколько это возможно, и не проще. Немного более сложный Reader and Writer сделает ваш процессор намного проще и поможет в обслуживании в будущем.

Код хорошо, мой друг.

Ссылка: Представляем образец делегата от нашего партнера JCG Рика Скарборо в блоге