Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Háromrétegű alkalmazás Spring keretrendszerben

Szűrő megjelenítése

Háromrétegű alkalmazás Spring keretrendszerben

A rétegekről

A legfontosabb kérdés persze az, hogy mi is ez a három réteg? Többféleképp is szokás őket nevezni, a legáltalánosabb talán a persistence, service és presentation megnevezések.

Three tier app schema

Ez utóbbi webes alkalmazások esetén az egyszerűség kedvéért gyakran a web megnevezést kapja. A három réteg egymástól teljesen független, mindnek megvan a saját felelősségköre, melyeket természetesen érdemes nem keverni. A rétegek egymásra épülnek, minden réteg függ az alatta levő rétegtől. A kereszthívásokat kerülni kell, tehát a presentation réteg nem szabad, hogy közvetlenül hívjon egy persistence rétegbeli eszközt (bár ezt sajnos a tranzitív függés miatt általában megtehetjük). A rétegek felelősségköre az alábbiak szerint alakul:

Persistence:

Mint a neve is mutatja, az alkalmazás perzisztencia kezeléséért felelős, tehát ez a réteg kommunikál az adatbázissal, állományokkal. Tipikusan csak primitív írási-olvasási műveletek kerülnek itt implementálásra. A perzisztencia réteg kódbázisa nem kell, hogy “döntéseket tudjon hozni” vagy komoly kalkulációkat végezzen, neki csak a felette levő réteg kéréseire kell válaszolni, méghozzá a perzisztens tárolóból származó adatokkal.

Service:

A service (szolgáltatás) réteg végzi a munka oroszlánrészét, hiszen itt kerül implementálásra minden üzleti logika. A service réteg a persistence rétegtől kapott adatokon operál, feldolgozza, transzformálja őket, végül továbbítja a presentation réteg felé. Vagy épp fordítva: a felhasználói interakciók eredményét a presentation elküldi a service-nek, az feldolgozza a nyers adatokat és küldi a persistence rétegnek, hogy az letárolhassa egy perzisztens tárolóban. A service döntéseket hoz, kalkulál, figyelembe veszi a felhasználó jogosultságait, illetve összekapcsolja a presentation és a persistence réteget.

Presentation:

A presentation réteg felelőssége a felhasználói interfész vezérlése; a service-től származó adatok megjelenítése, illetve a felhasználói interakciók továbbítása a service rétegnek. E réteg felelőssége továbbá a felhasználótól érkező “nyers” adatok validálása is, mely az adatok helyességének vizsgálatát, illetve a rendszer számára kártékony adatok kiszűrését jelenti. A presentation réteg azonban sokféle lehet: asztali alkalmazásfelület, mobil alkalmazásfelület, web UI, de egy REST interfész is presentation rétegnek minősül. Az utóbbi két esetben a presentation-t tipikusan web rétegnek szokás keresztelni.

A Spring eszközkészlete

A Spring mindhárom réteg implementálásában segítséget nyújt megfelelő eszközök biztosításával. Gyakorlatilag minden esetben speciális Java bean-ek jönnek létre, melyek konfigurációja történhet klasszikus (XML) alapon vagy annotációkkal is (lásd korábbi cikkemet: Spring XML konfiguráció cseréje Java alapúra). E cikkben annotációk használatával mutatom be az eszköz konfigurációkat. Vegyük sorra a lehetőségeket.

Persistence réteg:

Perzisztencia kezeléshez az egyik legegyszerűbben használható eszköz a Repository, mely a Spring Data csomagban található. A Repository-k a standard JPA (Java Persistence API) keretrendszerre épülő interfészek, melyek automatizált működésükkel jelentős mértékben képesek rövidíteni a perzisztencia réteg implementálását. Minden Repository két komponensből áll: egy entitás osztályból és egy repository interfészből. Az entitás osztályban kell megadni a mezőket, az azokra értelmezett megszorításokat (elsődleges / egyedi kulcs), és a köztük levő kapcsolatokat (külső kulcs). Míg a repository interfész az entitásokon végrehajtható műveleteket írja le.

Az entitás osztályokat @Entity annotációval kell megjelölni, ezeket fogja keresni az úgynevezett EntityManager, mely Spring Boot használata esetén automatikusan létrejön, anélkül manuálisan kell létrehozni egy @Configuration osztály alatt (később még erre visszatérünk). A megjelölt osztályban privát tagokként létre kell hoznunk a megfelelő mezőket, valamint az azokhoz tartozó getter és setter metódusokat. Ügyeljünk arra, hogy szükség lesz az alapértelmezett konstruktorra, így ha létrehozunk egy sajátot, hozzuk létre a paraméter-mentes konstruktort is. A létrehozott mezők leképezése az adatbázisban automatikusan történik, azonban ez a @Column annotáció használatával felülbírálható (illetve többek között ezzel az annotációval adható meg, hogy a mező egyedi kulcsértéket kell tartalmazzon). Az entitás azonosító mezőjét az @Id annotációval elsődleges kulccsá állíthatjuk, az entitások közötti kapcsolatokat (tehát a külső kulcsos megszorításokat) pedig a @OneToMany, @ManyToOne, @ManyToMany, @OneToOne, stb. annotációkkal adhatjuk meg. Megfelelően annotált entitások esetén a fizikai adatbázis létrehozása és kezelése automatikus lesz, a képződő táblákban az entitásokban leírt mezők szerepelnek majd, köztük az annotációk által meghatározott kapcsolatok és megszorítások állnak majd fenn.

@Entity
@Table(name = DatabaseConstants.TABLE_COMMENTS)
public class Comment extends SelfStatusAwareIdentifiableEntity<Long> {

	@ManyToOne
	@JoinColumn(name = DatabaseConstants.COLUMN_USER_ID,
			foreignKey = @ForeignKey(name = DatabaseConstants.FK_COMMENT_USER))
	private User user;
	@ManyToOne
	@JoinColumn(name = DatabaseConstants.COLUMN_ENTRY_ID,
			foreignKey = @ForeignKey(name = DatabaseConstants.FK_COMMENT_ENTRY))
	private Entry entry;
	@Column(name = DatabaseConstants.COLUMN_CONTENT, length = 2000)
	private String content;
	
	//  ...
}

A repository interfészek létrehozása a CrudRepository (csak alapvető CRUD műveletek), PagingAndSortingRepository (CRUD műveletek és lapozás) vagy JpaRepository (CRUD műveletek, lapozás és bulk CRUD műveletek) interfész kiterjesztésével kezdődik. Érdemes jól megválasztani, hogy melyiket használjuk, például ahol nincs szükség lapozásra, ott elégséges a CrudRepository interfészből kiindulni. Érdekesség, hogy nincs szükség implementációra, mivel a Spring reflexióval generálja a metódus specifikációk alapján a megfelelő SQL - illetve JPQL - kéréseket. A Spring Data JPA dokumentációjában részletesen le van írva, hogyan kell megírni a metódus specifikációkat, hogy az elvárt eredményt kapjuk - Spring támogatással rendelkező IDE használata esetén (pl. STS, IDEA Ultimate, stb.) pedig már az IDE is képes figyelmeztetni, ha nem megfelelő metódus specifikációt írtunk. A repository-k végül az @Autowired annotációval “bekötve” használhatóak az adatbázissal való kommunikációra.

@Repository
public interface CommentRepository extends JpaRepository {

	public Page<Comment> findByEntry(Pageable pageable,  Entry entry);
	
	// ...
}

A fenti példa kiegészíti az alap JpaRepository-t a kommentek bejegyzéshez kötött felolvasására szolgáló findByEntry metódussal. Implementációra nincs szükség, a Comment model alapján a Spring automatikusan készíti el azt hozzá.

A repository-k létezése azonban még kevés lehet. Spring Boot használata esetén a feltérképezés automatikus, a repository-k rögtön használhatóak, azonban Spring Boot nélkül szükség lesz a korábban említett EntityManager-re. Az EntityManager-nek tudnia kell, merre találja az entitás osztályokat és a repository interfészeket, illetve szüksége lesz egy perzisztencia “szolgáltatóra”, mely tipikusan a Hibernate JPA. A Hibernate feladata lesz a JPQL utasítások fordítása a megfelelő SQL dialektusra (MySQL, Oracle, Microsoft SQL, H2, MongoDB, stb.) Az EntityManager-nek továbbá szüksége lesz egy tranzakció kezelőre. Ha nincsenek különleges igények, a Spring által biztosított standard tranzakció kezelő meg fog felelni számunkra. Az utolsó, bár igazából a legfontosabb komponens a DataSource, mely a tényleges adatbázis-kapcsolat objektum - ezt is át kell adni majd az EntityManager-nek - a mellékelt példában természetesen bemutatom, hogyan:

// létrehozzuk a konfigurációs osztályt, melyet ismernie kell az alkalmazás kontextusának
// Az alábbi módon engedélyezzük a JPA repository-k használatát, valamint a tranzakciókezelést
@Configuration
@EnableJpaRepositories(basePackages = ConfigurationProperty.REPOSITORY_PACKAGE)
@EnableTransactionManagement
public class RepositoryConfig {
 
	// ...
 
	@Bean
	public DataSource dataSource() throws InitializationException {
 
		try {
		// szükségünk lesz egy DataSource objektumra, mely az adatbázis-kapcsolatot 
		// építi fel és kezeli
		// ez a konfiguráció-részlet a Leaflet nevű készülő alkalmazásomból lett kivágva, 
		// amiben JNDI forrásból adom át a DataSource objektumot
		JndiTemplate jndiTemplate = new JndiTemplate();
		DataSource dataSource = (DataSource) jndiTemplate.lookup(jdbcSource);
 
		// check if connection is alive
		dataSource.getConnection();
 
		return dataSource;
 
		} catch (SQLException | NamingException e) {
			throw new InitializationException(e);
		}
	}
 
	@Bean
	public EntityManagerFactory entityManagerFactory() throws InitializationException {
 
		// először is létre kell hozni az entity manager objektumot egy factory-vel.
		LocalContainerEntityManagerFactoryBean factoryBean = 
			new LocalContainerEntityManagerFactoryBean();
 
		// illetve szükségünk lesz a JPA implementációra, 
		// amit a Hibernate fog biztosítani
		HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
 
		// az alkalmazás számára szükség van néhány konfigurációs paraméterre
		Properties properties = new Properties();
		properties.put(PROPERTY_KEY_HIBERNATE_DDL_AUTO, hibernateDDLAuto);
		properties.put(PROPERTY_KEY_HIBERNATE_DIALECT, hibernateDialect);
 
		adapter.setShowSql(generateDDL);
		adapter.setGenerateDdl(showSQL);
 
		// végül a factory számára oda kell adnunk mindent, amit eddig létrehoztunk
		factoryBean.setJpaProperties(properties); // - a konfigurációs paramétereket
		factoryBean.setJpaVendorAdapter(adapter); // - a Hibernate adaptert
		// - a csomag nevét, ahol az entitások megtalálhatóak 
		factoryBean.setPackagesToScan(ConfigurationProperty.ENTITY_PACKAGE);
		factoryBean.setDataSource(dataSource()); // - a DataSource objektumot
		// - az adapter PersistenceProvider objektumát
		factoryBean.setPersistenceProvider(adapter.getPersistenceProvider());
		// - és a használandó SQL dialektust
		factoryBean.setJpaDialect(new HibernateJpaDialect()); 
		factoryBean.afterPropertiesSet();
 
		return factoryBean.getObject();
	}
 
	@Bean
	public PlatformTransactionManager transactionManager() throws InitializationException {
 
		// végül mindezt átadjuk egy tranzakciókezelőnek
		JpaTransactionManager txManager = new JpaTransactionManager();
		txManager.setEntityManagerFactory(entityManagerFactory());
 
		return txManager;
	}
}

Service réteg:

Ahogy azt már korábban említettem, a service rétegben helyezkedik el alkalmazásunk üzleti logikája. A helyzet itt egészen egyszerű: Minden service osztályt (tehát nem a service műveleteit leíró interface-t, hanem az implementáló osztályt) @Service annotációval kell ellátnunk, és természetesen egy @Configuration osztályban jeleznünk kell, melyik csomagban helyezkednek el ezek az osztályok. Az üzleti logika mellett ebben a rétegben kezeljük például a perzisztencia réteg kivételeit és a felhasználó jogosultságait is.

@Service
public class CommentServiceImpl implements CommentService {
 
	@Autowired
	private CommentRepository commentRepository;
	
	// ...
}

Presentation / web réteg:

A Spring és elsősorban a Spring Web a webes alkalmazások fejlesztése során nyújt igazi segítséget. Az alkalmazás “belépési pontjai” a @Controller annotációval ellátott osztályokban foglalnak helyet. A belépési pontok publikus metódusok, melyeket @RequestMapping annotációval jelölhetünk meg, mint HTTP kérés fogadására alkalmas belépési pontot. A @RequestMapping több paramétert is vár, melyek a következők:

  • method: a HTTP metódus típusa - egy vagy több metódus megadása lehetséges, ezzel szabályozható, mely HTTP metódusokra kell a belépési pontnak válaszolnia. A Spring alapból az alábbi metódusokat ismeri: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
  • path: a kontextus gyökértől értelmezett relatív erőforrás útvonal. Például ha az alkalmazás a /api kontextus gyökér alatt helyezkedik el, a path értéke pedig “/users”, akkor az elérési útvonal /api/users lesz. A path értékében megadhatóak paraméter holderek is, melyeket tetszőleges, kapcsos zárójelekkel körülvett paraméternév választ el az útvonal többi részétől. Ezeket később a metódus paraméter listájában a @PathVariable annotációval hivatkozva érjük el, mely paraméterül az útvonal megadott holder nevet várja. Például ha az útvonal /api/users/{userID} akkor a paraméter értéke a @PathVariable(“userID”) int userID változóban lesz elérhető.
  • consumes: a belépési pont által elfogadott médiatípusok listája adható meg. Például ha a consumes értéke “application/json”, a belépési pont csak “applicaton/json” médiatípusú kéréseket fog elfogadni.
  • produces: ez pedig a válasz médiatípusát adja meg. Sokkal inkább akkor van haszna, ha a megfelelő média-konverterek hozzá vannak adva a kontextushoz, mivel ez esetben a választ a Spring Web automatikusan képes konvertálni a megadott típusra. Például JSON típusú válasz létrehozásához a Jackson média-konverterre lesz szükségünk. Megjegyzés: @Controller helyett @RestController-t használva a Spring minden a controller osztályban megjelenő @RequestMapping “produces” mezőjét automatikusan “application/json” típusra állítja, így REST alapú alkalmazás fejlesztésekor egyszerűen használhatjuk ezt is.
  • headers: a HTTP kérés header-ökre való szűrés valósítható meg vele. Paraméternév=érték formában adhatóak meg, illetve használható negálás (!=), valamint a * wildcard is. Csak a mintára illeszkedő kérésekre fog a belépési pont válaszolni.
  • params: a “headers”-höz hasonlóan a megadott request paraméterek szűrésére szolgál.

A HTTP belépési pontként szolgáló metódusok paraméterei annotációk segítségével populálhatóak a request tartalmával. Így például egy URI request paraméter értéke a @RequestParam(“paraméternév”) annotációval, a request body tartalma a @RequestBody annotációval válik elérhetővé. JSON/XML típusú request body esetén szükség lesz a típusnak megfelelő média-konverter jelenlétére a kontextusban, így a nyers adatok automatikusan parse-olódnak a hozzájuk rendelt model osztály példányaként.

@RequestMapping(method = RequestMethod.POST, value = "/users")
@ResponseStatus(HttpStatus.CREATED)
public BaseBodyDataModel createUser(
	@RequestBody @Valid UserCreateRequestModel userCreateRequestModel, 
	BindingResult bindingResult) {
	
	// ...
}

A response body tartalma a “produces” mező értékétől és metódus megfelelően megválasztott return-jétől függ. A válasz értéke lehet egy ModelAndView példány (erre JSP ViewResolver esetén lesz szükség). Apache Tiles, Thymeleaf, és hasonló megjelenítő-framework-ök esetén általában egy nézetnévvel kell visszatérni. Ennek a nézetnévnek a ViewResolver által feloldhatónak kell lennie. REST/SOAP válasz esetén a visszatérési típus adott példánya kerül konvertálásra a megfelelő médiatípusra, és ez kerül a response body-ba.

Utószó

Utolsó cikkem meglehetőségen régen jelent meg, ennek pedig az az oka, hogy elkezdődött a Leaflet nevű blogrendszer fejlesztése, melyet néhány kollegával hegesztünk immáron közel fél éve. A cikkben látható példák kivételesen most ezen alkalmazásval kódjának részletei, helyenként némiképp módosítva, hogy fedje a cikk tartalmát. A fejlesztés eddigi munkálatai során rengeteg kisebb-nagyobb, érdekes megoldandó probléma merült fel, melyekből tervezek a közeljövőben cikkeket írni. Az alkalmazás publikus GitHub repositoryja itt található:

Alkalmazás forrása

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.