Récemment l'équipe CodeStory a lancé le concours pour la sélection 2013, informations ici.
Pour participer, il faut un serveur public qui répond à des requêtes HTTPs. Pour la première étape, il faut que le serveur réponde à la requête GET "http://foobar.com:9090/?q=Quelle+est+ton+adresse+email" avec votre adresse email.
Rien de bien compliqué (en tout cas pour le moment), mais il faut évidement que votre serveur puisse évoluer pour répondre aux prochaines questions.
Architecture technique
Pour répondre au besoin de CodeStory, il y a plusieurs solutions (en restant dans l'univers java) :
- Du JEE (ou conteneur de servlet simple) hébergé chez cloudbees ou à la maison.
- Du play hébergé chez heroku, cloudbees ou à la maison.
- Du Google App Engine.
- Ou beaucoup plus simple :)
Pour mettre en place cette "architecture", deux étapes très compliquées :
- Le pom.xml
- La classe main
Le pom.xml
Il faut juste ajouter la dépendance vers Jetty :
<dependency> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty</artifactId> <version>6.1.25</version> </dependency>
La classe main
Le boulot de la classe main est simplement de démarrer le serveur et traiter les requêtes HTTP :
package fr.ybonnel.codestory; import org.mortbay.jetty.Server; import org.mortbay.jetty.handler.AbstractHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class WebServer extends AbstractHandler { public static final String QUERY_PARAMETER = "q"; @Override public void handle(String target, HttpServletRequest request, HttpServletResponse httpResponse, int dispatch) throws IOException, ServletException { // Traitement de la requète. } public static void main(String[] args) throws Exception { int port = 10080; if (args.length == 1) { port = Integer.parseInt(args[0]); } Server server = new Server(port); server.setHandler(new WebServer()); server.start(); server.join(); } }
Les intérêts
L'intérêt d'une telle "architecture" est la simplicité, ce qui se traduit par trois avantages :
- Rapidité de démarrage : 38ms sur mon poste qui n'est pas un fourdre de guerre.
- Tests unitaires sans mock : grâce à la rapidité de démarrage, on peux faire des tests qui démarrent le serveur, exécutent une requête GET, et arrêtent le serveur. On se place donc à la place du client, ce qui est sans doute une garantie d'avoir le résultat attendu.
- Facilité d'installation : juste un jar à exécuter (donc très simple que ce soit dans l'IDE ou dans sur un serveur).
Tests unitaires
Comme on l'a vu, pour les tests unitaires, rien de bien sorcier :
- On démarre le serveur (dans une méthode @Before, pour qu'elle soit exécutée avant chaque test)
- On fait le test (envoi d'une requête GET, et vérifications sur la réponse).
- On arrête le serveur (dans une méthode @After).
Code complet du test de la première étape :
package fr.ybonnel.codestory; import com.google.api.client.http.*; import com.google.api.client.http.javanet.NetHttpTransport; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mortbay.jetty.Server; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import static junit.framework.Assert.assertEquals; public class WebServerTest { public static final int PORT = 18080; private Server server; @Before public void setup() throws Exception { WebServer.setTest(true); server = new Server(PORT); server.setHandler(new WebServer()); server.start(); new Thread(){ @Override public void run() { try { server.join(); } catch (InterruptedException ignore) { } } }.start(); } @After public void teardown() throws Exception { server.stop(); } @Test public void should_answear_to_whatsyourmail() throws Exception { String url = "http://localhost:" + PORT + "/?q=Quelle+est+ton+adresse+email"; HttpResponse response = sendGetRequest(url); assertEquals("Status code must be 200", 200, response.getStatusCode()); assertEquals("Response must be my mail", "ybonnel@gmail.com", responseToString(response)); } private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); private HttpResponse sendGetRequest(String url) throws IOException { HttpRequestFactory requestFactory = HTTP_TRANSPORT.createRequestFactory(); HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url)); return request.execute(); } private String responseToString(HttpResponse response) throws IOException { BufferedReader bufReader = new BufferedReader(new InputStreamReader( response.getContent(), response.getContentCharset())); StringBuilder builder = new StringBuilder(); String line = bufReader.readLine(); while (line != null) { builder.append(line); line = bufReader.readLine() } return builder.toString(); } }Pour les tests suivant, seule la méthode @Test est à réécrire.
Pour faciliter l'écriture des requêtes http, j'utilise la librairie google-http-client, mais si vous avez mieux, je suis preneur.
EDIT : j'utilise maintenant JWebUnit, beaucoup plus simple.
Déploiement
Je n'ai pas encore parlé d'hébergement, ce qui pour un serveur qui doit être accessible publiquement reste important.
Ayant un serveur dédié à disposition, je suis parti sur de l'auto-hébergement. Si vous me demandez "pourquoi", je vous répondrai "parce que"...
Afin de m'auto-héberger j'ai suivi trois étapes :
- Assemblage du jar
- Démarrage et arrêt
- Déploiement simplifié
Assemblage du jar
Mon build est sous maven, créer un jar contenant les dépendances n'est donc pas très compliqué, il suffit d'ajouter la configuration qui va bien dans le pom.xml :
<build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>fr.ybonnel.codestory.WebServer</mainClass> </manifest> </archive> <finalName>${artifactId}</finalName> <appendAssemblyId>false</appendAssemblyId> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build>
Scripts de démarrage et d'arrêt
Pour le démarrage et l'arrêt, je ne me suis pas cassé la tête :
- Démarrage :
java -jar code-story.jar >> serveur.log 2>&1 &
- Arrêt :
ps -ef | grep java | grep code-story | grep -v grep | while read a b c do kill -9 $b done
Déploiement simplifié
Le dernier truc auquel je tenais est un déploiement simple, je suis passé par deux étapes avec des logiques très différentes :
- Déploiement par update des sources
- Déploiement par git push
Déploiement par update des sources
Ma première façon de déployer était relativement simple, j'ai fait un clone de mon repo git sur le serveur. Donc pour redéployer, je faisait simplement un "git pull", suivi d'une compilation et restart du serveur.
Mon script de déploiement ressemblait donc à :
git pull mvn clean install assembly:singleIl suffisait ensuite de redémarrer le serveur pour prendre en compte le nouveau jar.
Quelques inconvénients cependant à cette technique :
- Il faut se connecter au serveur pour le mettre à jour.
- On mélange les sources et la partie serveur au même endroit
Déploiement par git push
J'ai eu envie que le déploiement se résume à un "git push serveur master" depuis mon poste de dev (fortement inspiré de la façon de déployer sur heroku).
Première étape, créer le repo git sur le serveur (depuis le serveur) :
mkdir CodeStory.git cd CodeStory.git git init --bareEt voilà, j'ai un repo git accessible par ssh.
Deuxième étape, pousser le contenu actuel sur le repo (depuis mon poste de dev) :
git remote add serveur ssh://ybonnel@XXX.XXX.XXX.XXX:XXXX/home/ybonnel/CodeStory.git git push serveur master
Troisième étape, créer la partie serveur (sur le serveur donc) :
mkdir CodeStory-server cd CodeStory-server/ cp ../CodeStory/target/code-story.jar . cp ../CodeStory/scripts/* .Mon répertoire "CodeStory-server" contient donc :
- Le jar
- Le script de démarrage et le script d'arrêt
Quatrième et dernière étape, créer le hook sur le repo git. Pour ce faire, j'ai créé le script "post-receive" dans "CodeStory.git/hooks" dont voici le contenu :
echo "Updating server..." rm -rf /home/ybonnel/CodeStory git clone /home/ybonnel/CodeStory.git /home/ybonnel/CodeStory cd /home/ybonnel/CodeStory ./updateServeur.sh echo "Update and restart of server are done"Et voici le contenu du script "updateServeur.sh" :
mvn clean install assembly:single if [ $? -eq 0 ] then cp scripts/* ../CodeStory-server/ cp target/code-story.jar ../CodeStory-server/code-story.jar.new cd ../CodeStory-server ./stopServeur.sh mv code-story.jar code-story.jar.old mv code-story.jar.new code-story.jar ./startServeur.sh sleep 1 tail -10 serveur.log fiUn fois ce hook mis en place, lorsque je fait un "git push serveur master" depuis mon poste de dev, une compile maven se lance, et si le build maven est OK, le serveur est mis à jour. Et je vois le résultat de la compile et du déploiement en direct lors de mon git push.
Et dans la vrai vie?
Maintenant vous allez me dire, c'est bien sympa ton truc, mais dans la vrai vie, les projets sont un peu plus compliqués que simplement fournir un email en réponse à un GET...
Les architectures Web modernes sont souvent composées d'une partie serveur qui répond du JSON, et une partie cliente qui joue avec (y a qu'à voir le succès de angular.js). Et avec des architectures de ce type, répondre du JSON est-il beaucoup plus compliqué que répondre une adresse email?
Pour information, mon site ybo-tv est hébergé sur un tomcat, mais il serait relativement facile de le basculer sur une architecture de ce type (pas de stack lourde juste pour répondre du JSON, c'est facilement faisable en spécifique).