Vamos a escribir una pequeña aplicación web, sin usar marcos web, bibliotecas externas y un servidor de aplicaciones.
El propósito de este artículo es mostrar la esencia general de lo que sucede bajo el capó de un servicio web, utilizando Java como ejemplo. Entonces vamos. No debemos usar bibliotecas de terceros ni servlets. Por lo tanto, el proyecto será ensamblado por Maven, pero sin dependencias.
¿Qué sucede cuando un usuario ingresa una determinada dirección IP (o dns que se convierte en una dirección IP) en la barra de direcciones del navegador? Se realiza una solicitud al ServerSocket del host especificado, en el puerto especificado.
Organizamos en nuestro host local, socket en un puerto libre aleatorio (por ejemplo 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; } }
No olvide que el oyente en el puerto, como un objeto, es deseable para nosotros en una sola copia, por lo tanto, singleton (no necesariamente doble verificación, pero puede ser así).
Ahora en nuestro host-e (localhost) en el puerto 9001, hay un escucha que recibe lo que el usuario ingresa como una secuencia de bytes.
Si resta byte [] del socket en DataInputStream y lo convierte en una cadena, obtendrá algo como esto:
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
Solicitud Http estándar con todos los encabezados necesarios.
Para el análisis, crearemos una pequeña interfaz de utilidad con métodos predeterminados, en mi opinión, es bastante conveniente para tales propósitos (además, si todavía es Spring, reduciremos el número de dependencias en la clase).
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) {
Esta utilidad puede analizar el tipo de solicitud, la url y una lista de parámetros para las solicitudes GET y POST.
En el proceso de análisis, formamos el modelo de solicitud, con la URL de destino y el Mapa con los parámetros de solicitud.
El controlador de nuestro servicio es una pequeña abstracción de la biblioteca en la que podemos agregar libros (en esta implementación, solo a la Lista), eliminar libros y devolver una lista de todos los libros.
1. Controlador
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); } }
También tenemos un controlador singleton.
Registramos RequestMapping. Detener, lo hacemos sin un marco, ¿qué RequestMapping? Tendremos que escribir esta anotación nosotros mismos.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequestMapping { String path() default "/"; }
También fue posible agregar la anotación del
Controlador sobre la clase y, al inicio de la aplicación, recopilar todas las clases marcadas con esta anotación y sus métodos, y agregarlas a un Mapa determinado desde las asignaciones de URL. Pero en la implementación actual nos limitaremos a un controlador.
Antes del controlador, tendremos un cierto preprocesador, que formará el modelo de solicitud comprensible para el programa y se asignará a los métodos del controlador.
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. Modelo
Como modelo tendremos un libro de clase
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 + '\''; } }
y servicio
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) {
que recopilará una colección de libros y colocará los datos del Modelo (algunos mapas) recibidos del servicio.
3. Ver
Como vista, crearemos una plantilla html y la colocaremos en un directorio de recursos / páginas separado, lo que aumentará el nivel de presentación.
<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>
Escribimos nuestro propio motor de plantillas, la clase debe poder evaluar la respuesta recibida del servicio y generar el encabezado http necesario (en nuestro caso, OK o MAL SOLICITUD), reemplazar las variables necesarias en el documento HTML con los valores del Modelo y finalmente representar el HTML completo que el navegador y el usuario pueden entender.
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 ""; } }
Como prueba de la aplicación para el rendimiento, agregamos un par de libros a nuestra aplicación:

Gracias por leer hasta el final, el artículo es solo orientativo, espero que haya sido un poco interesante y útil.