Nous allons écrire une petite application Web, sans utiliser de frameworks Web, de bibliothèques externes et d'un serveur d'applications.
Le but de cet article est de montrer l'essence générale de ce qui se passe sous le capot d'un service Web, en utilisant Java comme exemple. Alors allons-y. Nous ne devons pas utiliser de bibliothèques tierces, ainsi qu'un servlet. Par conséquent, le projet sera assemblé par Maven, mais sans dépendances.
Que se passe-t-il lorsqu'un utilisateur entre une certaine adresse IP (ou DNS qui se transforme en adresse IP) dans la barre d'adresse du navigateur? Une demande est faite au ServerSocket de l'hôte spécifié, sur le port spécifié.
Nous organisons sur notre hôte local, socket sur un port libre aléatoire (par exemple 9001).
public class HttpRequestSocket { private static volatile Socket socket; private HttpRequestSocket() { } public static Socket getInstance() throws IOException { if (socket == null) { synchronized (HttpRequestSocket.class) { if (socket == null) { socket = new ServerSocket(9001).accept(); } } } return socket; } }
N'oubliez pas que l'écouteur sur le port, en tant qu'objet, nous est souhaitable en une seule copie, donc singleton (pas forcément une double vérification, mais ça peut l'être).
Maintenant sur notre hôte-e (localhost) sur le port 9001, il y a un écouteur qui reçoit ce que l'utilisateur entre comme un flux d'octets.
Si vous soustrayez l'octet [] du socket dans DataInputStream et le convertissez en chaîne, vous obtenez quelque chose comme ceci:
GET /index HTTP/1.1 Host: localhost:9001 Connection: keep-alive Cache-Control: no-cache Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Postman-Token: 838f4680-a363-731d-aa74-10ee46b9a87a Accept: */* Accept-Encoding: gzip, deflate, br Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
Demande Http standard avec tous les en-têtes nécessaires.
Pour l'analyse, nous allons créer une petite interface utilisateur avec des méthodes par défaut, à mon avis, c'est assez pratique pour de telles fins (en plus, si c'est toujours Spring, alors nous réduisons le nombre de dépendances dans la classe).
public interface InputStringUtil { default String parseRequestMapping(final String inputData) { return inputData.split((" "))[1]; } default RequestType parseRequestType(final String source) { return valueOf(source.split(("/"))[0].trim()); } default Map<String, String> parseRequestParameter(final String source) { if (parseRequestType(source) == GET) { return parseGetRequestParameter(source); } else { return parsePostRequestParameter(source); } } @SuppressWarnings("unused") class ParameterParser { static Map<String, String> parseGetRequestParameter(final String source) { final Map<String, String> parameterMap = new HashMap<>(); if(source.contains("?")){ final String parameterBlock = source.substring(source.indexOf("?") + 1, source.indexOf("HTTP")).trim(); for (final String s : parameterBlock.split(Pattern.quote("&"))) { parameterMap.put(s.split(Pattern.quote("="))[0], s.split(Pattern.quote("="))[1]); } } return parameterMap; } static Map<String, String> parsePostRequestParameter(final String source) {
Cet utilitaire peut analyser le type de demande, l'URL et une liste de paramètres pour les demandes GET et POST.
Dans le processus d'analyse, nous formons le modèle de demande, avec l'url cible et la carte avec les paramètres de demande.
Le contrôleur de notre service est une petite abstraction de la bibliothèque dans laquelle nous pouvons ajouter des livres (dans cette implémentation, juste à la liste), supprimer des livres et renvoyer une liste de tous les livres.
1. Contrôleur
public class BookController { private static volatile BookController bookController; private BookController() { } public static BookController getInstance() { if (bookController == null) { synchronized (BookController.class) { if (bookController == null) { bookController = new BookController(); } } } return bookController; } @RequestMapping(path = "/index") @SuppressWarnings("unused") public void index(final Map<String, String> paramMap) { final Map<String, List<DomainBook>> map = new HashMap<>(); map.put("book", DefaultBookService.getInstance().getCollection()); HtmlMarker.getInstance().makeTemplate("index", map); } @RequestMapping(path = "/add") @SuppressWarnings("unused") public void addBook(final Map<String, String> paramMap) { DefaultBookService.getInstance().addBook(paramMap); final Map<String, List<DomainBook>> map = new HashMap<>(); map.put("book", DefaultBookService.getInstance().getCollection()); HtmlMarker.getInstance().makeTemplate("index", map); } }
Nous avons également un contrôleur singleton.
Nous enregistrons RequestMapping. Arrêtez, nous le faisons sans framework, quelle RequestMapping? Nous devrons écrire cette annotation nous-mêmes.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequestMapping { String path() default "/"; }
Il était également possible d'ajouter l'annotation
Controller sur la classe et, au démarrage de l'application, de collecter toutes les classes marquées avec cette annotation et leurs méthodes, et de les ajouter à une certaine carte à partir des mappages d'URL. Mais dans l'implémentation actuelle, nous nous limiterons à un seul contrôleur.
Avant le contrôleur, nous aurons un certain préprocesseur, qui formera le modèle de demande compréhensible par le programme et sera mappé aux méthodes du contrôleur.
public class HttpRequestPreProcessor implements InputStringUtil { private final byte[] BYTE_BUFFER = new byte[1024]; public void doRequest() { try { while (true) { System.out.println("Socket open"); final Socket socket = HttpRequestSocket.getInstance(); final DataInputStream in = new DataInputStream(new BufferedInputStream(socket.getInputStream())); final String inputUrl = new String(BYTE_BUFFER, 0, in.read(BYTE_BUFFER)); processRequest(inputUrl); System.out.println("send request " + inputUrl); } } catch (final IOException e) { e.printStackTrace(); } } private void processRequest(final String inputData) { final String urlMapping = parseRequestMapping(inputData); final Map<String, String> paramMap = parseRequestParameter(inputData); final Method[] methods = BookController.getInstance().getClass().getMethods(); for (final Method method : methods) { if (method.isAnnotationPresent(RequestMapping.class) && urlMapping.contains(method.getAnnotation(RequestMapping.class).path())) { try { method.invoke(BookController.getInstance(), paramMap); return; } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } } } HtmlMarker.getInstance().makeTemplate("error", emptyMap()); }
2. Modèle
Comme modèle, nous aurons un livre de classe
public class DomainBook { private String id; private String author; private String title; public DomainBook(String id, String author, String title) { this.id = id; this.author = author; this.title = title; } public String getId() { return id; } public String getAuthor() { return author; } public String getTitle() { return title; } @Override public String toString() { return "id=" + id + " author='" + author + '\'' + " title='" + title + '\''; } }
et service
public class DefaultBookService implements BookService { private static volatile BookService bookService; private List<DomainBook> bookList = new ArrayList<>(); private DefaultBookService() { } public static BookService getInstance() { if (bookService == null) { synchronized (DefaultBookService.class) { if (bookService == null) { bookService = new DefaultBookService(); } } } return bookService; } @Override public List<DomainBook> getCollection() { System.out.println("get collection " + bookList); return bookList; } @Override public void addBook(Map<String, String> paramMap) { final DomainBook domainBook = new DomainBook(paramMap.get("id"), paramMap.get("author"), paramMap.get("title")); bookList.add(domainBook); System.out.println("add book " + domainBook); } @Override public void deleteBookById(long id) {
qui collectera une collection de livres et mettra dans le modèle (une carte) les données reçues du service.
3. Afficher
En tant que vue, nous créerons un modèle html et le placerons dans un répertoire de ressources / pages séparé, ce qui augmentera le niveau de présentation.
<html> <head> <title>Example</title> </head> <br> <table> <td>${book.id}</td><td>${book.author}</td><td>${book.title}</td> </table> </br> </br> </br> <form method="get" action="/add"> <p>Number<input type="text" name="id"></p> <p>Author<input type="text" name="author"></p> <p>Title<input type="text" name="title"></p> <p><input type="submit" value="Send"></p> </form> </body> </html>
Nous écrivons notre propre moteur de modèle, la classe devrait être en mesure d'évaluer la réponse reçue du service, et de générer l'en-tête http nécessaire (dans notre cas, OK ou BAD REQUEST), remplacer les variables nécessaires dans le document HTML par les valeurs du modèle, et enfin rendre le HTML complet que le navigateur et l'utilisateur peuvent comprendre.
public class HtmlMarker { private static volatile HtmlMarker htmlMarker; private HtmlMarker() { } public static HtmlMarker getInstance() { if (htmlMarker == null) { synchronized (HtmlMarker.class) { if (htmlMarker == null) { htmlMarker = new HtmlMarker(); } } } return htmlMarker; } public void makeTemplate(final String fileName, Map<String, List<DomainBook>> param) { try { final BufferedWriter bufferedWriter = new BufferedWriter( new OutputStreamWriter( new BufferedOutputStream(HttpRequestSocket.getInstance().getOutputStream()), StandardCharsets.UTF_8)); if (fileName.equals("error")) { bufferedWriter.write(ERROR + ERROR_MESSAGE.length() + OUTPUT_END_OF_HEADERS + readFile(fileName, param)); bufferedWriter.flush(); } else { bufferedWriter.write(SUCCESS + readFile(fileName, param).length() + OUTPUT_END_OF_HEADERS + readFile(fileName, param)); bufferedWriter.flush(); } } catch (IOException e) { e.printStackTrace(); } } private String readFile(final String fileName, Map<String, List<DomainBook>> param) { final StringBuilder builder = new StringBuilder(); final String path = "src\\resources\\pages\\" + fileName + ".html"; try (BufferedReader br = Files.newBufferedReader(Paths.get(path))) { String line; while ((line = br.readLine()) != null) { if (line.contains("${")) { final String key = line.substring(line.indexOf("{") + 1, line.indexOf("}")); final String keyPrefix = key.split(Pattern.quote("."))[0]; for (final DomainBook domainBook : param.get(keyPrefix)) { builder.append("<tr>"); builder.append( line.replace("${book.id}", domainBook.getId()) .replace("${book.author}", domainBook.getAuthor()) .replace("${book.title}", domainBook.getTitle()) ).append("</tr>"); } if(param.get(keyPrefix).isEmpty()){ builder.append(line.replace("${book.id}</td><td>${book.author}</td><td>${book.title}", "<p>library is EMPTY</p>")); } continue; } builder.append(line).append("\n"); } return builder.toString(); } catch (IOException e) { e.printStackTrace(); } return ""; } }
Pour tester l'application pour les performances, nous ajoutons quelques livres à notre application:

Merci d'avoir lu jusqu'à la fin, l'article est uniquement à titre indicatif, j'espère qu'il était un peu intéressant et un peu utile.