Рубрики
Без рубрики

Spring Boot Часть 5: Вуайерист, Нетривиальное приложение

В этой серии статей рассматриваются особенности Spring Boot. Эта пятая статья из серии представляет собой n… С тегами java, spring, thymeleaf.

Эта серия из статей рассматривает Пружинный ботинок особенности. В этой пятой статье из серии представлено нетривиальное приложение, которое исследует локальные хосты (с помощью команды nmap ), чтобы помочь в разработке UPNP и SSDP приложения.

Полный источник и javadoc доступны на GitHub . Дополнительные артефакты (включая их соответствующие исходные файлы и банки javadoc) доступны из репозитория Maven по адресу:

    
      repo.hcf.dev-RELEASE
      hcf.dev RELEASE Repository
      https://repo.hcf.dev/maven/release/
      default
    

Конкретные темы, затронутые в настоящем документе:

Теория работы

В следующих подразделах описываются компоненты.

Реализации @Service

Пакет voyeur определяет ряд (аннотированных) @Сервис s :

ArpCache Сопоставление InetAddress с аппаратным адресом периодически обновляется путем чтения /proc/net/arp или анализа выходных данных arp -an
Сетевые интерфейсы Набор сетевых интерфейсов
Nmap Отображение выходных данных XML команды nmap для каждого адреса входа, обнаруженного с помощью кэша ARP, сетевых интерфейсов и/или SSDP
SSDP Узлы SSDP, обнаруженные с помощью кэша обнаружения SSDP

Каждая из этих служб реализует Набор или Карта , которая может быть @Autowire d в другие компоненты и периодически обновляют себя с помощью метода @Scheduled . Подробно рассматривается сервис Nmap .

Во-первых, метод @PostConstruct (в дополнение к выполнению других операций инициализации) проверяет, доступна ли команда nmap :

...
@Service
@NoArgsConstructor @Log4j2
public class Nmap extends InetAddressMap ... {
    ...
    private static final String NMAP = "nmap";
    ...
    private boolean disabled = true;
    ...
    @PostConstruct
    public void init() throws Exception {
        ...
        try {
            List argv = Stream.of(NMAP, "-version").collect(toList());

            log.info(String.valueOf(argv));

            Process process =
                new ProcessBuilder(argv)
                .inheritIO()
                .redirectOutput(PIPE)
                .start();

            try (InputStream in = process.getInputStream()) {
                new BufferedReader(new InputStreamReader(in, UTF_8))
                    .lines()
                    .forEach(t -> log.info(t));
            }

            disabled = (process.waitFor() != 0);
        } catch (Exception exception) {
            disabled = true;
        }

        if (disabled) {
            log.warn("nmap command is not available");
        }
    }
    ...
    public boolean isDisabled() { return disabled; }
    ...
}

Если команда nmap выполнена успешно, ее версия регистрируется. В противном случае disabled имеет значение true и никаких дальнейших попыток запустить команду nmap другими методами не предпринимается.

Метод @Scheduled update() вызывается каждые 30 секунд и гарантирует, что запись карты существует для каждого InetAddress , ранее обнаруженного NetworkInterfaces , , ARP Кэширует и SSDP компоненты , а затем ставит в очередь/| Работник Запускаемый для любого значения, выходной сигнал которого превышает ИНТЕРВАЛ (60 минут) от роду. В @EventListener ApplicationReadyEvent

public class Nmap extends InetAddressMap ... {
    ...
    private static final Duration INTERVAL = Duration.ofMinutes(60);
    ...
    @Autowired private NetworkInterfaces interfaces = null;
    @Autowired private ARPCache arp = null;
    @Autowired private SSDP ssdp = null;
    @Autowired private ThreadPoolTaskExecutor executor = null;
    ...
    @EventListener(ApplicationReadyEvent.class)
    @Scheduled(fixedDelay = 30 * 1000)
    public void update() {
        if (! isDisabled()) {
            try {
                Document empty = factory.newDocumentBuilder().newDocument();

                empty.appendChild(empty.createElement("nmaprun"));

                interfaces
                    .stream()
                    .map(NetworkInterface::getInterfaceAddresses)
                    .flatMap(List::stream)
                    .map(InterfaceAddress::getAddress)
                    .filter(t -> (! t.isMulticastAddress()))
                    .forEach(t -> putIfAbsent(t, empty));

                arp.keySet()
                    .stream()
                    .filter(t -> (! t.isMulticastAddress()))
                    .forEach(t -> putIfAbsent(t, empty));

                ssdp.values()
                    .stream()
                    .map(SSDP.Value::getSSDPMessage)
                    .filter(t -> t instanceof SSDPResponse)
                    .map(t -> ((SSDPResponse) t).getInetAddress())
                    .forEach(t -> putIfAbsent(t, empty));

                keySet()
                    .stream()
                    .filter(t -> INTERVAL.compareTo(getOutputAge(t)) < 0)
                    .map(Worker::new)
                    .forEach(t -> executor.execute(t));
            } catch (Exception exception) {
                log.error(exception.getMessage(), exception);
            }
        }
    }
    ...
    private Duration getOutputAge(InetAddress key) {
        long start = 0;
        Number number = (Number) get(key, "/nmaprun/runstats/finished/@time", NUMBER);

        if (number != null) {
            start = number.longValue();
        }

        return Duration.between(Instant.ofEpochSecond(start), Instant.now());
    }

    private Object get(InetAddress key, String expression, QName qname) {
        Object object = null;
        Document document = get(key);

        if (document != null) {
            try {
                object = xpath.compile(expression).evaluate(document, qname);
            } catch (Exception exception) {
                log.error(exception.getMessage(), exception);
            }
        }

        return object;
    }
    ...
}

Вводится Spring Boot ‘s ThreadPoolTaskExecutor . Чтобы гарантировать, что выделено более одного потока, application.properties содержит следующее свойство:

spring.task.scheduling.pool.size: 4

Реализация Worker приведена ниже.

    ...
    private static final List NMAP_ARGV =
        Stream.of(NMAP, "--no-stylesheet", "-oX", "-", "-n", "-PS", "-A")
        .collect(toList());
    ...
    @RequiredArgsConstructor @EqualsAndHashCode @ToString
    private class Worker implements Runnable {
        private final InetAddress key;

        @Override
        public void run() {
            try {
                List argv = NMAP_ARGV.stream().collect(toList());

                if (key instanceof Inet4Address) {
                    argv.add("-4");
                } else if (key instanceof Inet6Address) {
                    argv.add("-6");
                }

                argv.add(key.getHostAddress());

                DocumentBuilder builder = factory.newDocumentBuilder();
                Process process =
                    new ProcessBuilder(argv)
                    .inheritIO()
                    .redirectOutput(PIPE)
                    .start();

                try (InputStream in = process.getInputStream()) {
                    put(key, builder.parse(in));

                    int status = process.waitFor();

                    if (status != 0) {
                        throw new IOException(argv + " returned exit status " + status);
                    }
                }
            } catch (Exception exception) {
                remove(key);
                log.error(exception.getMessage(), exception);
            }
        }
    }
    ...

Обратите внимание, что InetAddress будет удален из Map если Процесс завершается с ошибкой.

Пользовательский интерфейс @Контроллер, модель и шаблон Thymeleaf

Полный Контроллер пользовательского интерфейса реализация приведена ниже.

@Controller
@NoArgsConstructor @ToString @Log4j2
public class UIController extends AbstractController {
    @Autowired private SSDP ssdp = null;
    @Autowired private NetworkInterfaces interfaces = null;
    @Autowired private ARPCache arp = null;
    @Autowired private Nmap nmap = null;

    @ModelAttribute("upnp")
    public Map> upnp() {
        Map> map =
            ssdp().values()
            .stream()
            .map(SSDP.Value::getSSDPMessage)
            .collect(groupingBy(SSDPMessage::getLocation,
                                ConcurrentSkipListMap::new,
                                mapping(SSDPMessage::getUSN, toList())));

        return map;
    }

    @ModelAttribute("ssdp")
    public SSDP ssdp() { return ssdp; }

    @ModelAttribute("interfaces")
    public NetworkInterfaces interfaces() { return interfaces; }

    @ModelAttribute("arp")
    public ARPCache arp() { return arp; }

    @ModelAttribute("nmap")
    public Nmap nmap() { return nmap; }

    @RequestMapping(value = {
                        "/",
                        "/upnp/devices", "/upnp/ssdp",
                        "/network/interfaces", "/network/arp", "/network/nmap"
                    })
    public String root(Model model) { return getViewName(); }

    @RequestMapping(value = { "/index", "/index.htm", "/index.html" })
    public String index() { return "redirect:/"; }
}

@Controller заполняет Модель с пятью атрибутами и реализует метод root 1 для обслуживания путей запросов пользовательского интерфейса. Суперкласс реализует getViewName() , который создает имя представления на основе пакета реализующего класса, которое преобразуется в classpath:/templates/voyeur.html , а Шаблон Thymeleaf для создания чистого документа HTML5. Его схема показана ниже.




  
    ...
  
  
    ...
    
...
...
...
...
...

Шаблон реализует меню и ссылается на пути, указанные в UI Controller.root() .

      

Шаблон также структурирован таким образом, чтобы создавать узел с узлом, соответствующим пути запроса, если в контексте нет переменной exception (нормальная работа). The th:переключатель и th:case атрибуты используются для создания <раздела/> , соответствующего каждому ${#request.servletPath} . <раздел/> , относящийся к пути /network/nmap , показан ниже:

    
...

...

Шаблон генерирует <таблицу/> со строкой ( ) для каждого ключа в Nmap . Каждая строка состоит из двух столбцов ( ):

  1. InetAddress хоста со ссылкой на вывод команды nmap 2 и список открытых TCP-портов

  2. Обнаруженные услуги/продукты

getport(InetAddress) и получить продукты(InetAddress) методы предоставляются, чтобы избежать XPath вычисления в шаблоне Thymeleaf .

...
@Service
@NoArgsConstructor @Log4j2
public class Nmap extends InetAddressMap ... {
    ...
    public Set getPorts(InetAddress key) {
        Set ports = new TreeSet<>();
        NodeList list = (NodeList) get(key, "/nmaprun/host/ports/port/@portid", NODESET);

        if (list != null) {
            for (int i = 0; i < list.getLength(); i += 1) {
                ports.add(Integer.parseInt(list.item(i).getNodeValue()));
            }
        }

        return ports;
    }
    ...
}

получить продукты(InetAddress) | реализация аналогична Xpathexpression из

Экземпляр UI Controller в сочетании с описанным до сих пор шаблоном Thymeleaf будет генерировать только чистый HTML5 без разметки стиля. Эта реализация использует функцию Thymeleaf Разделенная логика шаблонов и может быть найдена по адресу classpath:/templates/voyeur.th.xml . 3 Разделенная логика для таблицы, описанной в этом разделе, показана ниже.



  ...
  
    ...
    
      
        
          
          
        
      
    
    ...
  

Суперкласс UI Controller предоставляет еще одну функцию: вводить свойства, определенные в classpath:/templates/|.model.properties в Model/| .

brand = ${application.brand:}

stylesheets: /webjars/bootstrap/css/bootstrap.css
style:\
body { padding-top: 60px; margin-bottom: 60px; }\n\
@media (max-width: 979px) { body { padding-top: 0px; } }
scripts: /webjars/jquery/jquery.js, /webjars/bootstrap/js/bootstrap.js

Целью разработки этой реализации было зафиксировать всю логику разметки в *.th.xml ресурс, разрешающий только необходимость изменения несвязанной логики и свойств модели для использования альтернативной структуры. Эта цель была достигнута в этой реализации, поскольку разные фреймворки в разной степени поддерживают элементы HTML5. Частичная реализация Bulma доступна в https://github.com/allen-ball/voyeur/tree/trunk/src/main/resources/templates-bulma который демонстрирует различия в HTML5.

Запрос /network/map показан на изображении в Введении этой статьи.

вывод nmap @RestController

вывод nmap XML может быть обработан путем реализации @RestController . Класс Nmap аннотируется с помощью @RestController и @RequestMapping для запросов к /network/nmap/ и метод map(String) предоставляет XML, сериализованный в строку.

@RestController
@RequestMapping(value = { "/network/nmap/" }, produces = MediaType.APPLICATION_XML_VALUE)
...
public class Nmap ... {
    ...
    @RequestMapping(value = { "{ip}.xml" })
    public String nmap(@PathVariable String ip) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        transformer.transform(new DOMSource(get(InetAddress.getByName(ip))),
                              new StreamResult(out));

        return out.toString("UTF-8");
    }
    ...
}

Упаковка

У spring-boot-maven-plugin есть цель repackage , которая может быть использована для создания автономного JAR-файла со встроенным сценарием запуска. Эта цель используется в project pom для создания и прикрепления автономного артефакта JAR.


  ...
  
    
      
        ...
        
          org.springframework.boot
          spring-boot-maven-plugin
          
            
              
                build-info
                repackage
              
            
          
          
            true
            bin
            true
            ${start-class}
            
              ${basedir}/src/bin/inline.conf
            
          
        
        ...
      
    
    ...
  
  ...

Пожалуйста, ознакомьтесь с проектом GitHub страница для получения инструкций о том, как запустить JAR.

Резюме

В этой статье обсуждаются аспекты приложения voyeur и приводятся конкретные примеры:

  • @Service реализация и @Autowired компоненты с @Scheduled методами

  • @Controller реализация, Модель совокупность и Thymeleaf шаблоны и несвязанная логика

  • @RestController реализация

[1] В лучшем случае вводящее в заблуждение название метода. ↩

[2] Параметр @RestController описан в следующем подразделе. ↩

[3] общие свойства приложения не предоставляют возможности включить эту функциональность. Он включен в суперклассе UI Controller путем настройки введенного SpringResourceTemplateResolver . ↩

Оригинал: “https://dev.to/allenball/spring-boot-part-5-voyeur-a-non-trivial-application-327m”