Эта серия из статей рассматривает Пружинный ботинок особенности. В этой пятой статье из серии представлено нетривиальное приложение, которое исследует локальные хосты (с помощью команды nmap
), чтобы помочь в разработке UPNP и SSDP приложения.
Полный источник и javadoc доступны на GitHub . Дополнительные артефакты (включая их соответствующие исходные файлы и банки javadoc) доступны из репозитория Maven по адресу:
repo.hcf.dev-RELEASE hcf.dev RELEASE Repository https://repo.hcf.dev/maven/release/ default
Конкретные темы, затронутые в настоящем документе:
@Service
реализации с@Scheduled
обновленияПользовательский интерфейс
@Controller
- Заполняет
Модель
- Шаблоны Thymeleaf и несвязанная логика
- Заполняет
@RestController
реализация
Теория работы
В следующих подразделах описываются компоненты.
Реализации @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 ListNMAP_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
. Каждая строка состоит из двух столбцов ( ):
InetAddress
хоста со ссылкой на вывод командыnmap
2 и список открытых TCP-портовОбнаруженные услуги/продукты
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”