Spring Framework Series: Μερος 3ο

1. ΕΙΣΑΓΩΓΗ

Καλωσορίσατε στο 3ο μέρος της σειράς για το Spring Framework. Μέχρι τώρα είχαμε την ευκαιρία να δούμε τι είναι το Spring framework και πώς, πολύ γρήγορα, μπορούμε να στήσουμε ένα σημείο εκκίνησης για να κατασκευάσουμε μία web εφαρμογή βασισμένη στην πλατφόρμα αυτή. Πάμε τώρα να δούμε αναλυτικά τον τρόπο λειτουργίας του και τα επιμέρους κομμάτια μίας Spring MVC εφαρμογής.

ΕΤΟΙΜΟΙ; Φύγαμε!

2) Η ΚΑΡΔΙΑ ΤΗΣ MVC ΕΦΑΡΜΟΓΗΣ

Όπως είδαμε στο 1ο μέρος, μία Spring εφαρμογή βασίζεται σε έναν Spring IoC container το αρχέτυπο του οποίου είναι το interface org.springframework.context.ApplicationContext. Στην περίπτωση μίας Spring MVC εφαρμογής το implementation αυτού του interface είναι το WebApplicationContext. Πλήρως ενσωματωμένο με αυτό είναι το DispatcherServlet, το κεντρικό servlet (extends HttpServlet) πάνω στο οποίο είναι σχεδιασμένο το Spring Web MVC Framework. ToDispatcherServletείναι ουσιαστικά ένας front controller (βλέπε το “Front Controller” design pattern) που αναλαμβάνει την παραλαβή των requests και την προώθησή τους σε άλλους Controllers και Views.

Όπως όλα τα servlets σε μια τυπική web εφαρμογή, ο DispatcherServlet δηλώνεται μέσα στο web.xml μαζί με το mapping των url-patterns που θα εξυπηρετεί.

[xml]
<web-app>

<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/*.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<url-pattern>*.info</url-pattern>
</servlet-mapping>

</web-app>
[/xml]

Στο πιο πάνω δηλωτικό του web.xml βλέπουμε το τυπικό declaration του servlet μαζί με το αντίστοιχο mapping όπου στη συγκεκριμένη περίπτωση του δίνεται η οδηγία να αναλάβει το request handling όταν το URL έχει το pattern *.info. Επίσης παρατηρούμε ότι του περνάμε την οδηγία για το που να βρει τις παραμέτρους του ApplicationContext και συγκεκριμένα σε αρχεία τύπου xml που βρίσκονται στο /WEB-INF/spring/. Δίνεται επίσης η οδηγία να γίνει instantiated το servlet αυτό στο startup με προτεραιότητα 1.

3. ΤΟ ΚΥΚΛΩΜΑ SPRING WEB MVC

Πάμε να δούμε λοιπόν τι γίνεται μέσα σε μία Spring MVC εφαρμογή. Ο DispatcherServlet χρησιμοποιεί Beans (Controllers, Handlers, Resolvers κτλ.) του Spring Framework για να επεξεργαστεί τα requests και να τα περάσει στο αντίστοιχα views. Τα beans αυτά δηλώνονται στα παραμετρικά αρχεία που είδαμε λίγο πιο πάνω. Ας τα πάρουμε λοιπόν ένα, ένα:

1) Παραμετροποίηση

Ο front controller μας πρέπει να γνωρίζει τι θα κάνει με τα requests που του έρχονται. Αυτό επιτυγχάνεται με την παραμετροποίηση του στα αρχεία app-config.xml και mvc-config.xml που βρίσκονται στο /WEB-INF/spring/.

α) app-config.xml:

[xml]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<!– Scans within the base package of the application for @Components to configure as beans –>
<context:component-scan base-package="gr.zenika.web.app.mvctest" />

</beans>
[/xml]

Σε αυτό το πρώτο παραμετρικό αρχείο ο DispatcherServlet μαθαίνει ότι όλα beans βρίσκονται κάτω από το πακέτο gr.zenika.web.app.mvctest. Ουσιαστικά όταν γίνεται instantiated o Container του Spring, γίνεται ένα scanning σε αυτό το πακέτο και όσες classes έχουνε το annotation @Component ή το specialization αυτού όπως πχ. τα @Controller και @Repository λαμβάνονται υπόψη του container για reference από άλλα κομμάτια του Container ή συνδεόμενα με αυτό όπως πχ. ο DispatcherServlet.

β) mvc-config.xml:

[xml]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

<!– Configures support for @Controllers –>
<mvc:annotation-driven />

<!– Resolves view names to protected .jsp resources within the /WEB-INF/views directory –>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>

</beans>

[/xml]

Εδώ γίνονται 2 πράγματα.

Πρώτον δηλώνεται στον Container ότι το MVC είναι annotation-driven οπότε ο container ενεργοποιεί εσωτερικά 2 handlers (DefaultAnnotationHandlerMapping και AnnotationMethodHandlerAdapter) για την υποστήριξη beans με annotation δηλωτικά όπως το @Controller οπότε θα ξέρει ποιοι είναι πχ. οι Controllers.

Δεύτερον, δηλώνει ένα special bean του Spring MVC, τον InternalResourceViewResolver, που είναι ένας resolver για τα views του συστήματος τα οποία δηλώνεται ότι είναι jsp’s και βρίσκονται στο φάκελο /WEB-INF/views/. Προσέξτε πόσο έξυπνα το Spring ουσιαστικά αποκρύπτει (προστατεύει) τα jsp μας επιτρέποντας να τα βάλουμε στον απροσπέλαστο από τον browser WEB-INF φάκελο της εφαρμογής και με την βοήθεια αυτού του resolver επιτυγχάνεται η ελεγχόμενη προσπέλασή τους. Έμαθε λοιπόν ο DispatcherServlet για τους Controllers και τα Views του συστήματος και επομένως γνωρίζει που να προωθεί κάθε φορά τα requests.

2) Controller

Όπως δηλώθηκε στις παραμέτρους ποιο πάνω, οι Controllers βρίσκονται σε κάποιο subpackage του gr.zenika.web.app.mvctest και έχουν το δηλωτικό @Controller. Και για του λόγου το αληθές:

[java]
package gr.zenika.web.app.mvctest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Handles requests for the application welcome page.
*/
@Controller
@RequestMapping("/welcome.info")
public class WelcomeController {
/**
* Simply selects the welcome view to render by returning void and relying
* on the default request-to-view-translator.
*/
@RequestMapping(method = RequestMethod.GET)
public void welcome() { logger.info("Welcome!"); }
}

[/java]

Εδώ λοιπόν παρατηρούμε τα εξής:

α) Δηλώνεται ότι η class είναι ένας Controller με το @Controller

β) Δηλώνεται ότι αυτός ο Controller θα ενεργοποιηθεί μόνο για URL pattern /welcome.info, δηλαδή @RequestMapping(“/welcome.info”). Να θυμίσω ότι στο web.xml είχαμε δηλώσει ότι ο DispatcherServlet θα ασχοληθεί μόνο με ότι έχει *.info.

γ) H μέθοδος με τα οποία έρχονται Request Parameters είναι η GET,δηλαδή @RequestMapping(method = RequestMethod.GET)

3) Viewer

Όπως είδαμε πριν ο DispatcherServlet ξέρει ότι τα views βρίσκονται στο WEB-INF/views και έχουν κατάληξη .jsp. Εφόσον λοιπόν ο controller κοιτάει url patterns με /welcome.info, το jsp μας θα πρέπει να ονομάζεται welcome.jsp

.

Τη μετάφραση από /welcome.info σε /WEB-INF/views/welcome.jsp την αναλαμβάνει ο InternalResourceViewResolver που είδαμε πριν. Αν θέλαμε να μπερδέψουμε περαιτέρω επίδοξους hackers θα μπορούσαμε να προβούμε στις κατάλληλες ρυθμίσεις ώστε να εμφανίζεται το welcome.php !!! Πάμε λοιπόν να δούμε το output:

Να σημειώσω εδώ ότι στο default MVC project template που στήνει ο STS και είδαμε στο Μέρος 2 υπάρχει και ο URLRewriter για το οποίο έγραψα ένα review νωρίτερα. Αν και θεωρώ ότι έχει μεγάλες δυνατότητες ο συνδυασμός των δύο (ιδιαίτερα σε ασφάλεια και σε ακόμη πιο δυναμικό τροπο κατεύθυνσης των requests), ωστόσο προτείνω να μην το δοκιμάσετε αν δεν υπάρχει ακόμη η εμπειρία για το πώς λειτουργεί το URLRewriter.

4. REQUEST ΠΑΡΑΜΕΤΡΟΙ

Έχοντας δει μέχρι τώρα πώς λειτουργεί το κύκλωμα του Spring MVC πάμε να εξετάσουμε πώς γίνεται η διαχείριση των παραμέτρων του request. Ας υποθέσουμε λοιπόν ότι θέλουμε να γίνει εισαγωγή του ονόματός μας σε μία σελίδα και να εμφανιστεί σε μία δεύτερη. Πάμε καταρχήν να φτιάξουμε τα views.

α) inputname.jsp

[html]

<html>
<head>
<title>Welcome</title>
</head>
<body>
<h2>
Welcome to Spring !
</h2>
<FORM action="showname.info">

Please input your first name:&nbsp;
<input type="text" name = "firstName" size="50"/>

<input type="submit" />

</FORM>
</body>
</html>

[/html]

Η παραπάνω φόρμα θα αποστείλει σε ένα view που λέγεται showname.info showname.jsp

β) showname.jsp

[html]
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h2>
Welcome to Spring <i>${firstName}</i> !!!
</h2>
</body>
</html>

[/html]

Στη σελίδα αυτή εμφανίζεται το όνομα που αποστείλαμε πριν χρησιμοποιώντας JSTL variables. Πάμε τώρα να φτιάξουμε έναν controller που θα κάνει το handling του request. γ) TestController

[java]
package gr.zenika.web.app.mvctest;
import org.slf4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
* Handles requests for the application welcome page.
*/
@Controller
public class TestController {
private Logger logger = org.slf4j.LoggerFactory.getLogger(TestController.class);

@RequestMapping("/inputname.info")
public void inputname()
{
logger.info("Show inputname");
}

@RequestMapping( value="/showname.info", method = RequestMethod.GET )
public void showname(
@RequestParam(value = "firstName", required = false) final String firstName,
ModelMap model )
{
String fn = "Stranger";
if (firstName != null &amp;&amp; firstName.length() > 0){
fn = firstName;
}
model.addAttribute("firstName", fn);
logger.info("Added: "+firstName);
}
}
[/java]

Όπως βλέπουμε ένας Controller μπορεί να έχει παραπάνω από ένα view το οποίο κάνει handling αρκεί να βάλουμε το @RequestMapping πάνω από την κάθε μέθοδο που αντιστοιχεί σε αυτό το View. Το ενδιαφέρον εδώ εστιάζεται στην showname όπου:

α) Χρησιμοποιώντας το @RequestParam annotation “τσιμπάμε” το firstName από τα request parameters που έρχονται από τον DispatcherServlet.

β) Η παράμετρος ModelMap είναι μια LinkedHashMap στην οποία προστίθενται τα στοιχεία που θέλουμε να γίνουν exposed στο view. Αυτή θα μετατραπεί σε implicit object οπότε με την exrpession language της jsp μπορούμε να πάρουμε ό,τι προσθέσαμε δηλαδή το firstName (Θυμίζω ότι τα implict objects είναι αυτόματες variables στην jsp όπως το request, session κτλ.). Επίσης αντί για ModelMap μπορεί να χρησιμοποιηθεί η Model (ένα Java 5-specific interface που κρατάει μεταβλητές) και το Map.

γ) Το return είναι void αλλά θα μπορούσε να ήταν και άλλα πράγματα όπως ένα String με το όνομα του view, ένα Model, ένα Map κτλ.

δ) Το ποιο σημαντικό: Ουσιαστικά το declaration της μεθόδου αυτής (δηλαδή το όνομα + παράμετροι) είναι για το Spring frmawork ένα signature. Επομενώς οι συνδυασμοί που μπορούν να γίνουν είναι πάρα πολλοί. Στην version 3.0.x για παράδειγμα υποστηρίζονται τα RESTful Services για τα οποία έγραψε ένα εξαιρετικό άρθρο ο φίλος μου ο Αλέξιος. Αυτό είναι ένα καλό παράδειγμα των δυνατοτήτων που δίνεται στον χρήστη.

Αυτό ήταν ! Όλα παίζουν τώρα.

 

Στέλνοντας το όνομα παίρνουμε το ακόλουθο μήνυμα.

5. ΣΥΜΠΕΡΑΣΜΑ

Σε αυτό το άρθρο είδαμε σε λεπτομέρεια την δομή ενός απλού Spring MVC. Μέσα από τα 2 παραδείγματα είδαμε το flow της εφαρμογής step by step. Στηριζόμενοι πάνω σε αυτό το καλούπι μπορούν να χτιστούν οποιεσδήποτε λειτουργίες και να επεκταθούν οι απεριόριστες δυνατότητες (κυριολεκτώ) που δίνει το Spring.

Έχω ήδη ξεκινήσει να γράφω το 4ο άρθρο μου για για το Spring και συγκεκριμένα για το Webflow, μία τεχνολογία ΄που φέρνει το MVC στο επόμενο level !

Προς το παρόν επισυνάπτω μερικές ενδιαφέρουσες παραπομπές για περισσόερες λεπτομέρειες σχετικά με το Spring Web MVC.

Μαζί σας και πάλι λίαν συντόμως…

REFERENCES

Passionate Archer, Runner, Linux lover and JAVA Geek! That's about everything! Alexius Dionysius Diakogiannis is a Senior Java Solutions Architect and Squad Lead at the European Investment Bank. He has over 20 years of experience in Java/JEE development, with a strong focus on enterprise architecture, security and performance optimization. He is proficient in a wide range of technologies, including Spring, Hibernate and JakartaEE. Alexius is a certified Scrum Master and is passionate about agile development. He is also an experienced trainer and speaker, and has given presentations at a number of conferences and meetups. In his current role, Alexius is responsible for leading a team of developers in the development of mission-critical applications. He is also responsible for designing and implementing the architecture for these applications, focusing on performance optimization and security.