Il s'agit d'un projet qui a été créé dans le cadre d'un cours que j'ai suivi à l'Université de Varsovie. Il s'agit d'une simple implémentation d'un conteneur servlet qui prend en charge les servlets Java conformes à l'API de servlet Java. Même s'il est assez nu, il peut charger et exécuter des applications simples qui utilisent des fonctionnalités de base des servlets, y compris la répartition, le JSP et les servlets asynchrones.
Je n'ai pas trouvé beaucoup d'autres petites implémentations de conteneurs de servlets, alors n'hésitez pas à l'utiliser comme exemple de la façon de créer un conteneur comme celui-ci. Gardez à l'esprit qu'en raison des restrictions de temps, c'est un projet assez imparfait et buggy et même si j'ai essayé de rester fidèle à l'API servlet, j'ai pris des libertés ici et là.
L'idée est que ce conteneur fonctionne de manière similaire à Tomcat. Vous pouvez ajouter des classes de servlet à l'aide de servletContainer :: Addroute, mais la manière préférée est d'utiliser la fonctionnalité de balayage des composants. C'est-à-dire si l'application est zippée dans le fichier warName.war et placé dans le répertoire deploy dans server/src/main/resources il sera chargé sur le démarrage du serveur et disponible sous localhost:8000/warName .
Par défaut, le serveur s'exécute sur le port 8000. Il peut être modifié dans la fonction Main . Cette fonction contient également et l'exemple de la façon de démarrer et de configurer ce serveur.
Le projet a été construit à l'aide de Gradle. Il contient deux sous-projets: serveur et application de démonstration (base de données de livres simples). Il a été testé sous Linux et je ne sais pas si cela fonctionne même sous Windows / MacOS.
Pour exécuter l'utilisation du serveur:
./gradlew server:run
Le serveur de démarrage nous oblige à créer une instance de ServletContainer .
ServletContainer(int threads) Crée une nouvelle instance de ServletContainer . avec le nombre donné de threads utilisés pour créer un threadpool. Ce pool sera utilisé pour gérer les demandes HTTP.
void ServletContainer::start(int port) Commence le conteneur servlet sur le port donné. Gardez à l'esprit que les fichiers .war ne sont pas chargés à ce stade. Ils ne sont chargés que lorsque ServletContainer:servletScan est appelé.
void ServletContainer::stop()Arrête gracieusement le conteneur.
void ServletContainer::servletScan() Charge tous les fichiers .war à partir du répertoire deploy dans server/src/main/resources . Il chargera également les fichiers .jsp et les transpilera immédiatement dans des fichiers .class. Seules les classes annotées avec @WebServlet seront chargées. Une mise en garde est que les fichiers .war que j'utilise ont une structure de répertoire spécifique qui, je ne suis pas sûr, est la même que dans la plupart des autres fichiers .war. Vous pouvez consulter war Gradle Task in Demo Application pour voir à quoi devrait ressembler cette structure. Cette méthode doit être appelée au plus une fois avant Start () Méthode.
void ServletContainer::servletScan(String url) Identique à ServletContainer::servletScan() mais peut charger des fichiers .war à partir de l'URL donnée.
void ServletContainer::addRoute(Class<? extends HttpServlet> cls, String path)Ajoute un servlet au conteneur. Le servlet ne gérera qu'une URL donnée (voir la FAQ pour plus de détails).
Le serveur prend en charge la manipulation simultanée de plusieurs clients à la fois. Chaque client sera manipulé par Thread séparé de Threadpool. La taille du pool par défaut est 4 et peut être modifiée dans la fonction Main .
Le serveur prendra en charge toute classe qui hérite de HttpServlet. Une telle classe peut être ajoutée à l'aide de ServletContainer::addRoute .
Les fonctionnalités les plus importantes de ces classes sont mises en œuvre. Vous pouvez écrire au client en utilisant (uniquement) PrintWriter , définir des en-têtes et l'état de réponse. HTTPServletRequest peut extraire la méthode URL de demande, HTTP (obtenir, post, supprimer, patch), paramètres de requête et paramètres du corps (dans le cas du post). Les données sont envoyées au client après avoir rincé le tampon ou fermé la HttpServletResponse . À l'aide de RequestDispatcher , vous pouvez envoyer des requêtes à d'autres servlets, y compris les servlets JSP.
Il y a un support pour les servlets asynchrones. Après que HttpServletRequest::startAsync a été exécuté, la demande passe en mode asynchrone. Il existe deux façons d'utiliser ce mode. Tout code s'exécutera tant AsyncContext::complete est exécuté à un moment donné, qui met fin à la connexion au client. Vous pouvez également utiliser AsyncConext::start(Runnable) , ici vous devez également exécuter AsyncContext::complete à un moment donné. Ce dernier est compatible avec l'API Java Servlet. Il prend également en charge le délai d'attente ( AsyncContext::setTimeout ) et AsyncListener . Les servlets asynchronisés sont implémentés à l'aide CompletableFuture afin qu'ils utilisent Threadpool séparé de celui utilisé pour les clients synchrones.
Si l'application a été zippée dans .war et déplacée dans le dossier deploy , elle sera chargée automatiquement. Toutes les classes annotées avec @WebServlet et héritage de HttpServlet seront ajoutées au conteneur servlet et seront disponibles sur localhost:port/warName/servletUrl . Chaque classe doit avoir exactement un servletUrl spécifié dans l'annotation @WebServlet dans l'attribut de valeur. Si l'application utilise JSP, elle sera automatiquement compilée en .Class et chargée. De nombreuses applications peuvent être chargées, cependant, je ne sais pas ce qui se passe lorsqu'il y a les mêmes noms de classe utilisés dans deux applications.
Le serveur peut transpiler les fichiers .jsp sur .class. Il y a une prise en charge de presque toute la syntaxe dans JSP. Cela comprend:
<%@ page import/include=... %><% ... %><%! ... %><%= ... %><%-- %>${...} ${} Syntax prend partiellement le langage d'expression. Des opérations arithmétiques simples peuvent être effectuées et toute expression de l'instance de instance.property1.property2 sera convertie en request.getAttribute("instance").getProperty1().getProperty2() . Dans <% ... %> il y a aussi out.println(...) qui écrit directement au client. JSP peut être affiché à l'aide de RequestDispatcher::forward ou est disponible directement sur localhost:8000/warName/jspFileName.jsp . Un exemple d'action JSP est disponible sur localhost:8000/library/jsp . Gardez à l'esprit que j'ai implémenté l'analyse de moi-même afin que le formatage / syntaxe de code étrange puisse le casser.
Pour démontrer le conteneur servlet, j'ai implémenté une application simple qui simule une base de données de livres. Des livres peuvent être ajoutés, supprimés et mis à jour à l'aide de formulaires HTML. Tous les livres peuvent également être consultés dans la table HTML. Le frontend est entièrement développé à l'aide de JSP. L'application est zippée dans la bibliothèque.war à l'aide de la tâche war Gradle, déplacée pour deploy et chargé sur le serveur lors de son démarrage.
Points de terminaison:
Il y a plus de 30 tests pour vérifier la plupart des fonctionnalités et l'application de démonstration. Il est préféré les gérer d'Intellij en raison des problèmes expliqués en FAQ . Alternativement, ils peuvent être exécutés en utilisant:
./gradlew test
Ce projet faisait partie d'un cours universitaire et je n'ai pas été autorisé à utiliser des bibliothèques tierces à l'exception de JUnit.
Il y a des problèmes avec les ports de socket déjà utilisés. Je ne suis jamais descendu pour le réparer, mais la plupart du temps, cela fonctionne lorsqu'il est exécuté d'Intellij.
À l'heure actuelle, la résolution dont le servlet devrait gérer l'URL donnée est assez primitive. Fondamentalement, le serveur url est donné un servlet dont servletUrl est un préfixe d' url . S'il y en a beaucoup, celui avec le préfixe le plus long est sélectionné. Cela signifie que si un servlet a servletUrl égal à /app/home il gérera également /app/home/nonexisting/ , /app/home/12345 etc.
Tout le monde est invité à contribuer à ce projet. J'ai créé quelques problèmes de bon nombre, alors n'hésitez pas à les vérifier :)