Spring Boot ile Örnek Web Uygulaması

(Bu yazı Kod Gemisi Blog arşivinden alınmış olup, 25 Ağustos 2015 tarihinde Sedat Gökcen tarafından yazılmıştır.)

Bu yazıda size Spring Boot ile ilgili kısa bir bilgi verip, geliştirdiğim demirbaş takip uygulamasını aşama aşama anlatmaya çalışacağım.


Spring Boot Nedir?

Spring Boot, Spring tabanlı uygulama geliştirmenin en hızlı ve kolay yolu olması amacıyla geliştirilmiş bir frameworktür. Spring Boot sayesinde boilerplate yani basmakalıp kodlardan sıyrılıp, sadece ihtiyacımız olan kodu yazıyoruz (only write code that you need to). Spring Boot web sunucusu olarak Tomcat ve diğer birçok ek özellikle beraber geliyor.

Spring Boot’un sağladığı en büyük avantajlardan biri ise sizi herhangi bir XML konfigürasyonuyla uğraşmak zorunda bırakmaması.

Uygulama Gereksinimleri ve POM Dosyası

Spring Boot ile yazacağımız uygulamamız, şu gibi gereksinimleri karşılayacak:

  1. Kullanıcı adı unique olmak şartıyla, kayıtları databasede tutulan basit bir üyelik sistemi,
  2. Sadece sisteme giriş yapmış üyelerin görebileceği birer item ve kullanıcı listesi
  3. Yine sadece üyelerin müdahale edebileceği, database’e yeni item ekleme, item çıkarma ve itemın sorumluluğunu herhangi bir kullanıcıya atama

Yukarıdaki 3 gereksinimde de karşımıza çıkmasına rağmen ben güvenlik ve yetkilendirme aşamalarını (üye kaydı-girişi vs.) en sona bırakıp, öncelikle item ekleme, item çıkarma ve itemı herhangi bir kullanıcıya atama işlemlerini anlatmaya çalışacağım. Uygulamanın kaynak kodlarına buradan ulaşabilirsiniz.

İşe, ihtiyaç duyacağımız kütüphaneleri kullanabilmek için gerekli dependencyleri pom.xml dosyasına eklemekle başlayalım:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kodgemisi.webapps</groupId>
<artifactId>inventory-</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

spring-boot-starter-data-jpa ve h2 uygulamamızdaki database gereksinimi için, thymeleaf ise view katmanında template engine olarak kullanacağımız Thymeleaf için gerekli dependencyler. Daha sonra pom.xml dosyamıza iki dependency daha ekleyeceğiz.


Spring Boot ile İlk Web Sayfası

Her şeyden önce dosya yapımızı aşağıdaki gibi ayarlayalım:

Dilerseniz dosya yapımızı oluşturduktan sonra Model katmanındaki classları oluşturmaya geçmeden ana sayfamızı oluşturalım. Bunun için ihtiyacımız olanlar resources/templates altında home.html sayfası ile controller altında HomeController ve ana dizinde Application classları.

@Controller
public class HomeController {
@RequestMapping("/")
public String getHomePage() {
return "home";
}
}

HomeController şu aşamada gayet basit, sadece home viewunu döndürüyor, bunu daha sonra değiştirip, o anda giriş yapmış olan User‘ı da döndürmesini sağlayacağız.
RequestMapping(“/”) ile http://localhost:8080/ adresine bir GET isteği yaparak, hazırlayacağımız home.html sayfasına bu adresten erişilmesini sağlıyoruz.
Peki, ana sayfaya http://localhost:8080/home adresinden de erişmek istiyorsak ne yapacağız? Çözüm çok basit. RequestMapping, parametre olarak bir String arrayi alabiliyor. Yani yapacağımız işlem şu:

@RequestMapping(value = {"/", "/home"})

Şimdi de home viewumuza bir göz atalım:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Inventory Home</title>
</head>
<body>
<nav role="navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/users">Users</a></li>
<li><a href="/items">Items</a></li>
</ul>
</nav>

<h1>Welcome!</h1>
</body>
</html>

Şu an home.html sayfamız standart bir HTML sayfası, sizin de 2. satırdan anlayacağınız üzere bu sayfada Thymeleaf kullanacağız. Bunu sonraya bırakıyor ve Spring Boot ile hazırladığımız web uygulamasını ayağa kaldırmak için ihtiyacımız olan son classımızı yazıyoruz:

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

Application, uygulamamızın üzerinde çalışacağı main methodunu barındıran bir class olacak.

İşlem tamam! Şimdi uygulamamızı IDE üzerinden çalıştırıp, http://localhost:8080/ adresine gittiğimizde bizi Welcome! yazısı bekliyor olmalı. Bunun yerine herhangi bir hatayla karşılaşıyorsanız lütfen önceki adımları tekrar dikkatle inceleyin, pom.xml‘deki dependencylerin ve dosya yapınızın yukarıdakiyle aynı olduğundan emin olun.

! Uyarı !

Tabii ki dosya yapınız benimkiyle birebir aynı olmak zorunda değil, package isimleriniz ve class isimleriniz farklılık gösterebilir. Burada değineceğim asıl nokta şu: dosya yapınızın beklenenden çok farklı olması bazı durumlarda uygulamanızın çalışmasını etkileyebilir. Örnek vermek gerekirse, Application classının doğrudan src/java altında olması “Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package” gibi bir hata almanıza ve uygulamanızın çalışmamasına sebep olacak.

Spring Boot ile ilk web sayfamızı hazırladık. Sıra geldi uygulamamızın gereksinimlerine.


Item Ekleme

Senaryomuzu hazırlamak için gerekli yapıları katman katman hazırlayalım.

Model

Item ekleme senaryosu için ihtiyacımız olan ilk model sizin de tahmin edeceğiniz üzere Item. domain package’ı altında Item classımızı oluşturalım.

@Entity
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, updatable = false)
private long id;
@Column(name = "code", nullable = false, updatable = false, unique = true)
private String inventoryCode;
@Column(name = "type", nullable = false)
private String type;
public Item() {
}
public Item(String inventoryCode, String type) {
this.inventoryCode = inventoryCode;
this.type = type;
}
}

Gördüğünüz gibi Item modelimiz aynı zamanda oluşturacağımız database için bir Entity olacak. @GeneratedValue anotasyonuyla primary key olan id‘yi otomatik olarak oluşturuyoruz. inventoryCode, her item için farklı olacak bir stok kodu, bu kodu item’ı database’e ekleme aşamasında random bir string üreterek oluşturacağız. type‘ı ise bilgisayar, telefon, yatak vs. gibi itemın cinsi olarak düşünebilirsiniz.

Ben yazıdaki kod kısmını mümkün olduğunca kısa tutmak istememden dolayı set ve get methodlarını yukarıya eklemedim, siz kendi projenizde eklemeyi unutmayın.

Oluşturmamız gereken bir adet modelimiz daha var: ItemAddForm

Bu modeli, View katmanında hazırlayacağımız item ekleme formunun modeli olarak düşünebilirsiniz. ItemAddForm‘un görevi ihtiyacımız olan datayı View katmanından alıp Service katmanına taşımak. Bu taşıma işini yapan objelere DTO (Data Transfer Object) deniyor. DTO ile Model katmanında kullandığımız ana objelerin arasındaki fark, DTO’ların datayı alma ve datayı kaydetme harici (set ve get methodları) herhangi bir sorumluluğu olmaması, yani üzerlerinde başka method tanımlanmamasıdır.

Şimdi yine domain altında ItemAddForm classını oluşturalım.

public class ItemAddForm {
@NotEmpty
@Size(min=2, max=50)
private String itemType;
@NotNull
private int amount = 1; //default
public String getItemType() {
return itemType;
}
public void setItemType(String itemType) {
this.itemType = itemType;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}

@NotEmpty, @NotNull ve @Size Hibernate Validator anotasyonları. Bunları kullanarak kullanıcıdan aldığımız bilginin Service’e taşınmadan istediğimiz şekilde olduğuna emin olmuş oluyoruz. String tipindeki itemType için @NotNull değil, @NotEmpty anotasyonunu kullandığımıza dikkat edin.

Service

Service katmanının ne olduğunu, ne iş yaptığını açıklamadan önce, Service implementasyonlarında bolca kullanacağımız repository kavramını bir cümleyle tanımlayacak olursak: Repository, database’de tuttuğumuz dataya erişim objemiz, yani bilinen adıyla DAO (Data Access Object).

Biz kendi senaryomuz için ItemRepository adında bir interface oluşturup, bu interfacede CrudRepository‘i extend ederek save, delete, find gibi methodları Item entitymiz üzerinde kullanılabilecek duruma geleceğiz.

repository package’ı altında ItemRepository adında bir interface oluşturalım.

public interface ItemRepository extends CrudRepository<Item, Long> {
}

Item, üzerinde işlem yapacağımız entity, Long tahmin edeceğiniz üzere bu entity’nin idsinin type’ı.

Şu anlık ItemRepository‘e kendimiz herhangi bir method tanımı eklemeyeceğiz, yukarıda da bahsettiğim üzere CrudRepository‘den gelen save, delete, find vs. gibi methodlar bizim için yeterli. Repositorymizi oluşturduğumuza göre Service katmanına geçelim.

Service, aslında Controller’da olabilecek business logic’i encapsulate etmek için yarattığımız, Controller ve Model arasında duran bir katmandır. Service katmanının sorumluluklarını model objesini almak, yaratmak, güncellemek olarak sıralayabiliriz.

service package’ı altında ItemService adında bir interface ve ItemServiceImpl adında bir class oluşturalım.

public interface ItemService {
void addItem(ItemAddForm form);
}
@Service
public class ItemServiceImpl implements ItemService {
private final ItemRepository itemRepository;
private final UserService userService;
@Autowired
public ItemServiceImpl(ItemRepository itemRepository, UserService userService) {
this.itemRepository = itemRepository;
this.userService = userService;
}
public void addItem(ItemAddForm form) {
for (int i = 0; i < form.getAmount(); i++) {
String inventoryCode = Long.toHexString(Double.doubleToLongBits(Math.random())).substring(10); //generate random string
Item item = new Item(inventoryCode, form.getItemType());
itemRepository.save(item);
}
}
}

ItemAddForm‘dan bize itemın type’ı (bilgisayar, telefon vs.) ve bu itemdan stoka kaç tane ekleneceği bilgisi geliyor. Bu bilgilere göre Item objesini yaratıp, save methoduyla database’e ekliyoruz.

inventoryCode‘u nasıl oluşturduğumuza takılmayın, o satırda yaptığımız tek işlem 6 karakter uzunluğunda alfanümerik bir string oluşturmak.

Controller

controller package’ı altında ItemController classını oluşturalım.

@Controller
public class ItemController {
private final ItemService itemService;
private final UserService userService;
@Autowired
public ItemController(ItemService itemService, UserService userService) {
this.itemService = itemService;
this.userService = userService;
}
@RequestMapping("/items/add")
public ModelAndView itemAddPage() {
return new ModelAndView("addItem", "itemForm", new ItemAddForm());
}
@RequestMapping(value = "/items", method = RequestMethod.POST)
public String handleItemAdd(@Valid @ModelAttribute("itemForm") ItemAddForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors())
return "addItem";
itemService.addItem(form);
return "redirect:/items";
}
}

itemAddPage methodunda http://localhost:8080/items/add adresine GET isteği yapıp item ekleme sayfasına ulaşıyoruz. Burada HomeController‘daki gibi sadece String döndürmememizin sebebi, View’un yanında bir de ItemAddForm modeline ihtiyacımız olmamız. Yarattığımız ModelAndView objesinin constructorında “addItem” viewun adını, “itemForm” bu viewda kullanacağımız modelin adını, yarattığımız ItemAddForm objesi ise anlayacağınız üzere bu viewda kullanacağımız modeli belirtiyor. Bu kısımda biraz kafanız karışmış olabilir ama viewumuzu yani addItem.html‘i oluşturduğumuzda her şeyi daha net anlayacağınızı düşünüyorum.

itemAddPage methodunda hazırlayacağımız view’u gösterme işini hallettik, handleItemAdd methodunda ise adından da anlaşılacağı üzere item ekleme işini hallediyoruz. Bu methoddaki @Valid anotasyonunu ve bindingResult parametresini form validationı için kullanıyoruz. Validation kısmında herhangi bir sorun olmadığı takdirde ItemService‘in içinde oluşturduğumuz addItem methodu ile itemımızı database’e ekliyoruz.

View

Item ekleme senaryosunu tamamlamak için ihtiyacımız olan son katmanı hazırlayalım şimdi de. resources/templates altında addItem.html sayfasını oluşturalım.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Add Item</title>
</head>
<body>
<nav role="navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/items">Items</a></li>
</ul>
</nav>

<form th:action="@{/items}" th:object="${itemForm}" th:method="post">
<table>
<tr>
<td>Item Type:</td>
<td><input type="text" th:field="*{itemType}" /></td>
<td th:if="${#fields.hasErrors('itemType')}" th:errors="*{itemType}">Item Type Error will appear here</td>
</tr>
<tr>
<td>Amount:</td>
<td><input type="text" th:field="*{amount}" /></td>
<td th:if="${#fields.hasErrors('amount')}" th:errors="*{amount}">Amount Error will appear here</td>
</tr>
<tr>
<td><button type="submit">Add</button></td>
</tr>
</table>
</form>
</body>
</html>

Yukarıda standard HTML harici gördüğünüz, th ile başlayan her blok Thymeleaf’e ait. Sırayla açıklamak gerekirse eğer:

  • th:action‘daki URL’in, Controllerda POST isteğini yaptığımız handleItemAdd methodunda kullandığımız RequestMapping‘in değeriyle aynı olduğuna dikkat edin. @ sembolü bir URL expression, Thymeleaf kulladındığımız HTML sayfalarında link paylaşmak istediğimizde bu sembolü kullanıyoruz.
  • th:object alanında variable expression’ı olan $ sembolünü kullanarak, Controller’da @ModelAttribute anotasyonuyla belirttiğimiz ItemAddForm objesini kullanacağımızı belirtiyoruz.
  • th:field alanında ise kullanıcıdan gelen inputu, th:object‘te belirttiğimiz objenin hangi attributeuna set edeceğimizi yazıyoruz. Burada kullandığımız sembolün * olduğuna dikkat edin.
  • th:if ile başlayan blok oldukça açık. Form validationında herhangi bir sıkıntı olması durumunda hatayı yazdırıyoruz.

Kullandığımız Thymeleaf expressionlarıyla ilgili daha fazla bilgiye buradan, Spring için Thymeleaf dökümantasyonuna ise buradan ulaşabilirsiniz. Dökümantasyonların gayet açıklayıcı olduğunu söyleyebilirim.

Sonunda ilk gereksinimimizi tamamladık, şimdi test edelim. ItemServiceImpl classındaki addItem methodunda, for döngüsünün son satırına aşağıdaki satırı ekleyelim:

System.out.println(itemRepository.findOne(item.getId()));

findOne methodu, parametre olarak verdiğimiz id‘ye sahip Item‘ı databaseden bulup döndürecek. Item classımıza toString methodunu ekleyelim, daha doğrusu bu methodu override edelim.

@Override
public String toString() {
return "Item{" +
"id=" + id +
", inventoryCode='" + inventoryCode + '\'' +
", type='" + type + '\'' +
'}';
}

Şimdi uygulamamızı IDE üzerinden çalıştırıp, http://localhost:8080/items/add adresine gidip, istediğimiz miktarda ve typeta item ekleyelim. POST isteğini http:localhost:8080/items sayfasına yaptığımızdan bu sayfaya yönlendiriliyoruz. Bu sayfayı henüz hazırlamadığımız için Request method ‘GET’ not supported gibi bir hata alacaksınız, önemsemeyin, bu sayfayı sonraki aşamada hazırlayacağız. IDE’nize geri döndüğünüzde, konsolda eklediğiniz itemları göreceksiniz:

Item{id=1, inventoryCode='eb3e43', type='bilgisayar'}
Item{id=2, inventoryCode='a85420', type='bilgisayar'}
Item{id=3, inventoryCode='a9993c', type='bilgisayar'}

Şimdi sıra geldi eklediğimiz itemları http:localhost:8080/items adresinde listelemeye.


Itemları Listeleme ve Silme

Bu kısım çok daha kısa sürecek, hemen başlayalım.

Service

ItemService interfaceimize aşağıdaki 2 method tanımını ekleyelim:

Iterable<Item> getItems();
void deleteItemById(long id);

Şimdi ise, ItemServiceImpl classında bu methodların implementationlarını yapalım:

@Override
public Iterable<Item> getItems() {
return itemRepository.findAll();
}
@Override
public void deleteItemById(long id) {
itemRepository.delete(id);
}

Gördüğünüz gibi aslında bizim yaptığımız pek bir şey yok. CrudRepository‘den gelen methodları Service içinde kullanıyoruz. Bunu yapmamızın sebebi Controller’da hem Service hem Repository kullanmak yerine, sadece Service’i kullanmak istememiz.

Controller

ItemController‘a aşağıdaki iki methodu ekleyelim.

@RequestMapping("/items")
public ModelAndView getItemsPage() {
return new ModelAndView("items", "items", itemService.getItems());
}
@RequestMapping(value = "/items/{id}", method = RequestMethod.DELETE)
public String handleItemDelete(@PathVariable Long id) {
itemService.deleteItemById(id);
return "redirect:/items";
}

getItemsPage methodunda, items.html sayfasına, ismi items olan modelimizi gönderiyoruz. Bu kez modelimiz databasedeki tüm itemlar, bu itemlara ItemService içerisindeki, az önce yazdığımız getItems methoduyla erişiyoruz.

handleItemDelete methodunda ise itemı silme işlemini kontrol ediyoruz. http://localhost:8080/items/{id} adresine(örneğin, http://localhost:8080/items/1) DELETE isteği yapıp, adreste belirttiğimiz id değişkenindeki değere sahip itemı siliyoruz. URL’de kullandığımız bu değişkenlere PathVariable diyoruz. Silme işlemi tamamlandıktan sonra kullanıcıyı tekrar http://localhost:8080/items adresine yönlendiriyoruz.

View

resources/templates altında items.html sayfasını oluşturalım.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Items</title>
</head>
<body>
<nav role="navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/items/add">Add item</a></li>
</ul>
</nav>
<table>
<tr>
<th></th>
<th>Item code</th>
<th>Item type</th>
</tr>
<tr th:each="item : ${items}">
<td>
<form th:action="@{/items/} + ${item.id}" th:method="delete">
<button type="submit">Delete</button>
</form>
</td>
<td th:text="${item.inventoryCode}">Item code will appear here</td>
<td th:text="${item.type}">Item type will appear here</td>
</tr>
</table>
</body>
</html>

Thymeleaf’te th:each ile java.util.List objelerini, java.util.Iterable‘ı implement eden objeleri, java.util.Map‘i implement eden objeleri ve arrayleri iterate edebilirsiniz. Databasedeki her item için delete butonu ile itemın stok kodunu ve typeını yazdırıyoruz. Delete işlemi için Controller’da belirttiğimiz gibi http://localhost:8080/items/{id} adresine DELETE isteği yapıyoruz.

Itemin sorumluluğunu herhangi bir usera atama hariç, itemlarla ilgili gereksinimlerimizi tamamladık. Sıra geldi userlar ile ilgili gereksinimlere.

User Kaydı ve Userları Listeleme

Her zaman olduğu gibi Model katmanıyla başlıyoruz.

Model

domain package’ı altında User classımızı oluşturalım.

package app.domain;
import org.hibernate.validator.constraints.NotEmpty;
import javax.persistence.*;
import javax.validation.constraints.Size;
import java.util.Set;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, updatable = false)
private long id;
@NotEmpty
@Size(min = 3, max = 20)
@Column(name = "username", nullable = false, unique = true)
private String username;
@NotEmpty
@Size(min = 6, max = 20)
@Column(name = "password", nullable = false)
private String password;
@NotEmpty
@Column(name = "name", nullable = false)
private String name;
@NotEmpty
@Column(name = "lastName", nullable = false)
private String lastName;
@OneToMany(mappedBy = "user")
private Set<Item> items;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
}

Burada açıklama yapmayı gerek duyduğum tek kısım @OneToMany anotasyonu kulladığımız items Seti. Tahmin edeceğiniz üzere, User ve Item aralarında one-to-many ilişkisi olan birer Entity. User‘ın itemlarını bu Set içerisinde belirtiyoruz. Şimdi de Item tarafına, itemın sahibi User‘ı ekleyelim:

@ManyToOne
@JoinColumn(name = "user_id")
private User user;

@OneToMany anotasyonu hakkında daha fazla bilgiye buradan ulaşabilirsiniz, ben fazla üzerinde durmayacağım.

Modelimiz hazır olduğuna göre her zaman olduğu gibi sırada Service katmanı var.

Service

Öncelikle, repository altında UserRepository interfaceimizi oluşturalım.

public interface UserRepository extends CrudRepository<User, Long> {

}

Aynı ItemRepository’de yaptığımız gibi burayı şimdilik boş bırakalım ve service package’ı altında UserService’i oluşturalım:

public interface UserService {
void addUser(User user);
Iterable<User> getUsers();
}

UserServiceImpl ise:

@Controller
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/register")
public ModelAndView getRegisterPage() {
return new ModelAndView("register", "user", new User());
}
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String handleRegisterForm(@Valid @ModelAttribute("user") User user, BindingResult bindingResult) {
if (bindingResult.hasErrors())
return "register";
userService.addUser(user);
return "redirect:/";
}
@RequestMapping("/users")
public ModelAndView getUsersPage() {
return new ModelAndView("users", "users", userService.getUsers());
}
}

Controllerımız da yine ItemController ile çok benzer. Dikkat ederseniz bu sefer UserAddForm gibi bir DTO oluşturup, viewa o DTO’yu göndermek yerine doğrudan bir User objesi gönderdik. Yazının iyice uzamasını istemediğimden ve eğer oluştursaydım, UserAddForm, User objesi ile birebir aynı attributelara sahip olacağından bu kez DTO oluşturma işini pas geçtim. Siz dilerseniz bu kısımda da bir DTO oluşturabilirsiniz.

View

resources/templates altında register.html ve users.html sayfalarını yaratalım.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>User Registration</title>
</head>
<body>
<nav role="navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>

<form th:action="@{/register}" th:object="${user}" method="post">
<table>
<tr>
<td>User Name:</td>
<td><input type="text" th:field="*{username}" /></td>
<td th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Username Error</td>
<td th:if="${#fields.hasGlobalErrors()}" th:errors="*{global}">Global Error</td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" th:field="*{password}" /></td>
<td th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Password Error</td>
</tr>
<tr>
<td>Name:</td>
<td><input type="text" th:field="*{name}" /></td>
<td th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Error</td>
</tr>
<tr>
<td>Last Name:</td>
<td><input type="text" th:field="*{lastName}" /></td>
<td th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}">Last Name Error</td>
</tr>
<tr>
<td><button type="submit">Submit</button></td>
</tr>
</table>
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Jobs</title>
</head>
<body>
<nav role="navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/items">Items</a></li>
</ul>
</nav>
<table>
<tr>
<th>User name</th>
<th>Name</th>
<th>Last name</th>
</tr>
<tr th:each="user : ${users}">
<td><a th:href="@{/users/} + ${user.id} + @{/items}" th:text="${user.username}">Username</a></td>
<td th:text="${user.name}">Name</td>
<td th:text="${user.lastName}">Last Name</td>
</tr>
</table>
</body>
</html>

Bunlar da Item için hazırladığımız sayfalarla çok benzer, açıklayacak yeni bir şey olduğunu düşünmüyorum. http://localhost:8080/register adresinde user kaydı yapıp, bu userların listesine http://localhost:8080/users adresinden ulaşabilirsiniz.

User kaydı ve listeleme işlemi tamam, ama unutmayın şu an yaptığımız tek şey kayıt formu doldurulduğunda, User‘ı database’e kaydetmek. Sayfaları kayıtlı olmayan userlara kapatma, kullanıcı girişi gibi güvenlik işlemlerini en sona bırakıp, bir sonraki gereksinimimize geçiyorum.


Itemları Kullanıcıya Atama

Itemları ve userları başarıyla ekledik, peki ama hangi item hangi usera ait? x kişisinin sorumlu olduğu itemlar neler? Bu soruların cevaplarını verebilmek için yeni senaryomuza geçelim.

Model katmanına geçmeden, bu senaryoyu tamamladığımızda nasıl bir görüntü oluşturmak istediğimize bakalım:

Gördüğünüz üzere items sayfamızda bir takım değişiklikler yapacağız. Ama sırayı bozmayalım ve yine Model katmanıyla başlayalım.

Model

Bu kısımda ItemAssignForm adında bir DTO yaratacağız.

public class ItemAssignForm {
private String username;
}

Gördüğünüz gibi sadece tek attributeumuz var, bunun sebebini daha sonra anlayacaksınız. Ben her zamanki gibi set ve get methodlarını buraya eklemedim.

Service

Yukarıdaki ekran görüntüsünde gördüğünüz üzere, bir select box içerisinde bütün kullanıcı adlarını listeliyoruz. Bunun için getUsernames gibi bir methoda ihtiyacımız var.

UserService’e aşağıdaki method tanımını ekleyelim:

List<String> getUsernames();

Şimdi, UserServiceImpl‘da ise bunun implementasyonunu yapalım:

public List<String> getUsernames() {
List<String> usernames = new ArrayList<String>();
Iterator iterator = getUsers().iterator();
while (iterator.hasNext()) {
User user = (User) iterator.next();
usernames.add(user.getUsername());
}
return usernames;
}

Artık kullanıcı adlarının listesi elimizde. Hatırlayacağınız üzere, User’ın bir item listesi, Item‘ın ise bir userı vardı. Yani kullanıcı Assign butonuna tıkladığında olmasını beklediğimiz şeyler, userın item listesine ilgili itemın eklenmesi ve bunun yanında, o itemın userı olarakta select boxdan seçtiğimiz userın set edilmesi. Bunun için ItemService‘e aşağıdaki method tanımlarını ekliyoruz:

Item getItemById(long id);
Item assignItem(String username, long itemId);

Bunların implementationları ise:

public Item getItemById(long id) {
return itemRepository.findOne(id);
}
public Item assignItem(String username, long itemId) {
User user = userService.getUserByUsername(username);
Item item = getItemById(itemId);
Set<Item> itemList = user.getItems();
itemList.add(item);
user.setItems(itemList);
item.setUser(user);
return itemRepository.save(item);
}

Burada henüz yazmadığımız, UserService‘e ait bir getUserByUsername methodu var, bu methodu az sonra yazacağız. assignItem methodunu kısaca açıklayacak olursak, ItemAssignForm‘dan gelecek olan kullanıcı adına sahip userın item listesine, itemId‘ye sahip Item‘ı ekliyoruz. Daha sonra ise, bu itemın userını, getUserByUsername methoduyla aldığımız user’a set ediyoruz. En son yaptığımız işlem ise item’ın yeni halini database’e eklemek (Esasında buna bir update işlemi demek daha doğru olur).

getUserByUsername methodunu implement etmeden önce, UserRepository‘e aşağıdaki method tanımını ekliyoruz:

User findByUsername(String username);

Normal bir Java uygulamasında, yukarıdaki methodun implementasyonunu yapmamız gerekirdi. Spring Data JPA sayesinde bunu yapmamıza gerek kalmıyor, o bizim için bu implementasyonu, method ismine göre kendisi yaratıyor. Özetle, bizim sadece tanımını yaptığımız findByUsername methodu, parametre olarak verdiğimiz username‘e sahip userı bulacak database sorgusunu yazıp, userı bize getiriyor. Şimdi yukarıda kullandığımız getUserByUsername methodunu yazalım.

UserService:

User getUserByUsername(String username);

UserServiceImpl:

public User getUserByUsername(String username) {
return userRepository.findByUsername(username);
}

Service kısmında işimiz bitti, şimdi Controller’a geçelim.

Controller

Yukarıda http://localhost:8080/items sayfasını değiştireceğimizi söylemiştim. Şu anki haliyle, Controllerdan bu sayfaya sadece itemları gönderiyoruz. Ama yeni halinde, iki modele daha ihtiyacımız olacak. Birisi DTO’muz olan ItemAssignForm, diğeri ise select boxda göstermeyi hedeflediğimiz kullanıcı adları.

Peki, bir sayfaya birden fazla model nasıl göndereceğiz?

@RequestMapping("/items")
public ModelAndView getItemsPage() {
Map<String, Object> model = new HashMap<String, Object>();
model.put("items", itemService.getItems());
model.put("usernames", userService.getUsernames());
model.put("assignForm", new ItemAssignForm());
return new ModelAndView("items", model);
}

ItemController‘daki getItemsPage methodumuzun yeni hali böyle. Gördüğünüz gibi, birden fazla model göndermek için bir <String, Object> mapi oluşturup, modellerimizi bu mape ekledik.

ItemController‘daki yeni methodumuz ise:

@RequestMapping(value = "/items/{id}", method = RequestMethod.PUT)
public String handleItemAssign(@ModelAttribute("user") ItemAssignForm form, @PathVariable("id") long id) {
itemService.assignItem(form.getUsername(), id);
return "redirect:/items";
}

Bu aşamada aslında bir update işlemi yaptığımız için, PUT isteği yolluyoruz. assignItem methodunun parametreleri DTO’dan gelen usernamei ve PathVariable olan item id‘sini kullanıyoruz. İşlem tamamlandıktan sonra kullanıcıyı items sayfasına yönlendiriyoruz.

View

Sıra geldi items.html sayfasındaki değişiklikleri yapmaya.

<table>
<tr>
<th></th>
<th>Item code</th>
<th>Item type</th>
<th>Belongs to</th>
<th></th>
</tr>
<tr th:each="item : ${items}">
<td>
<form th:action="@{/items/} + ${item.id}" th:method="delete">
<input type="submit" value="Delete" name="delete" />
</form>
</td>
<td th:text="${item.inventoryCode}">Item code</td>
<td th:text="${item.type}">Item type</td>
<td th:text="${item.user}">Belongs to</td>
<td>
<form th:action="@{/items/} + ${item.id}" th:method="put">
<select th:field="*{assignForm.username}">
<option th:each="username : ${usernames}"
th:value="${username}"
th:text="${username}">Username will appear here
</option>
</select>
<button type="submit">Assign</button>
</form>
</td>
</tr>
</table>

<table>’ın içeriğini yukarıdaki gibi değiştirdikten sonra bu gereksinimimizi de tamamlamış oluyoruz. Burada da açıklayacak pek bir şey yok, daha önce hazırladığımız formlardan tek farkı bir select box içermesi.

Artık eklediğimiz itemları, userlara atayabiliyoruz. Sıra geldi atadığımız bu itemları, kullanıcıların sayfasında göstermeye.


User Sayfası Oluşturup, Itemlarını Listeleme

Buradaki amacımız, her user için ayrı sayfa oluşturup, userların sorumlu olduğu itemları kategorilerine göre listelemek.

Bu kısımda Model katmanıyla bir işimiz yok, doğrudan Service katmanıyla başlıyoruz.

Service

UserService interfaceine aşağıdaki method tanımlarını ekleyelim:

User getUserById(long id);
Map<String, List<Item>> numberOfItemsByType(long userId);

Bu methodların implementasyonları ise şöyle:

@Override
public User getUserById(long id) {
return userRepository.findOne(id);
}
public Map<String, List<Item>> numberOfItemsByType(long userId) {
Map<String, List<Item>> map = new HashMap<String, List<Item>>();
Set<Item> items = getUserById(userId).getItems();
for (Item item: items) {
List<Item> itemList = new ArrayList<Item>();
String key = item.getType().toLowerCase();
if (map.containsKey(key))
itemList = map.get(key);
itemList.add(item);
map.put(key, itemList);
}
return map;
}

numberOfItemsByType methodunda <String, List<Item>> mapi oluşturuyoruz. Bu mapte keyimiz itemın typeı, valuemuz ise o typetaki itemların bir listesi. Bu senaryomuzdaki tüm işi neredeyse bu method yapıyor.

Controller

@RequestMapping(value = "/users/{id}/items")
public ModelAndView getUserPage(@PathVariable Long id) {
if (null == userService.getUserById(id))
throw new NoSuchElementException("User with id:" + id + " not found");
else
return new ModelAndView("userItems" ,"items", userService.numberOfItemsByType(id));
}

Controllerda öncelikle URL’deki id‘ye sahip bir user olup olmadığını kontrol ediyoruz. Eğer böyle bir user yoksa exception throw ediyoruz, eğer varsa da yukarda yazdığımız methodun döndürdüğü mapi viewumuza gönderiyoruz.

View

resources/templates altında userItems.html sayfasını oluşturalım:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>User</title>
</head>
<body>
<nav role="navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/users">Users</a></li>
</ul>
</nav>

<div th:each="entry : ${items}" th:with="itemList=${entry.value}">
<p><strong th:text="${entry.key} + ': ' + ${itemList.size()}"></strong></p>
<table>
<tr>
<th>Item code</th>
<th>Item type</th>
</tr>
<tr th:each="item : ${itemList}">
<td th:text="${item.inventoryCode}">Item code</td>
<td th:text="${item.type}">Item type</td>
</tr>
</table>
</div>
</body>
</html>

Bu sayfada ilk defa karşılaştığımız şey, th:with ifadesi. th:with ile yukarıda döndürdüğümüz map’in valuesu olan item listesini itemList isimli bir değişkene atıyoruz. Aşağıda ise, bu itemList’teki her itemin inventoryCode’unu ve type’ını tek tek yazdırıyoruz.

Bu kısmıda tamamladık, artık her user için özel bir sayfamız var ve bu sayfada kullanıcıların sorumluluğundaki itemları listeleyebiliyoruz.

Artık uygulamamızı tamamlamak için son bir adım kaldı.


User Girişi

Öncelikle, pom dosyamıza aşağıdaki dependencyleri ekleyelim:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity3</artifactId>
<version>2.0.1</version>
<scope>compile</scope>
</dependency>

İlk dependencye, uygulamamızı güvenli hale getirmek ve yetkisiz kullanıcılara sayfaya giriş izni vermemek için ihtiyacımız var. İkinci dependency ise bize Thymeleaf’in bazı ekstra özelliklerini kullanabilmemizi sağlayacak.

Model

UserDetails‘in görevi, aşağıdaki methodlardan da anlayacağınız üzere core user bilgisini bulundurmaktır. User modelimizde UserDetails interfaceini implement edelim ve şu methodları ekleyelim:

@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("USER");
List<SimpleGrantedAuthority> list = new ArrayList<SimpleGrantedAuthority>();
list.add(simpleGrantedAuthority);
return list;
}

Aslında, UserDetails interfaceinde getUsername ve getPassword methodları da var ama biz zaten bu methodları modelimizi ilk oluşturduğumuzda yazmıştık.

getAuthorities methodunda sistemdeki yetkilerin bir listesini oluşturup döndürüyoruz. Bizim uygulamamızda bütün kullanıcıların yapabildiği işlemler aynı, bu yüzden USER adında sadece bir yetki ekliyoruz listeye. Örneğin, bu listeye ADMIN adında bir yetki ekleyebilir ve bu yetkiye sahip kullanıcıların, istedikleri kullanıcıyı sistemden silmesine olanak verebilirdik ama bu uygulama için böyle bir gereksinimimiz yok.

Username’in sistemimizde unique olacağını belirtmiştik. Dilerseniz, register sayfasına bir validator ekleyelim ve girilen kullanıcı adının sistemde zaten var olup olmadığını kullanıcıya bildirelim.

domain package’ı altında validator package’ı oluşturup, RegisterValidator classını yaratıyoruz:

@Component
public class RegisterValidator implements Validator {
private final UserService userService;
@Autowired
public RegisterValidator(UserService userService) {
this.userService = userService;
}
@Override
public boolean supports(Class<?> aClass) {
return aClass.equals(User.class);
}
@Override
public void validate(Object o, Errors errors) {
User form = (User) o;
validateUsername(errors, form);
}
private void validateUsername(Errors errors, User form) {
if (userService.getUserByUsername(form.getUsername()) != null) {
errors.reject("username.exists", "User with this username already exists");
}
}
}

Gördüğünüz gibi, bizim burada kontrol ettiğimiz tek şey girilen kullanıcı adının sistemde olup olmadığı. Eğer biz register sayfamızda onay için şifrenin iki kez girilmesini istesek, burada girilen iki şifrenin birbiriyle aynı olup olmadığını kontrol edebilirdik.

Model kısmıyla işimiz bitti, Service kısmına geçelim.

Service

UserServiceImpl classımızda UserDetailsService interfaceini implement ediyoruz ve aşağıdaki methodu ekliyoruz:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = getUserByUsername(username);
if (null == user) {
throw new UsernameNotFoundException("User with username: " + username + " not found.");
} else {
return user;
}
}

UserDetailsService, Spring Security’nin user girişini (user, gerçekten var olan login formunu mu kullanıyor, girilen password doğru mu, user’ın sistemdeki rolü veya yetkileri nedir gibi) loadUserByUsername methoduyla kontrol ettiği bir interface.

Controller ve Security Config

Öncelikle LoginController classını oluşturalım:

@Controller
public class LoginController {
@PreAuthorize("isAnonymous()")
@RequestMapping(value = "/login")
public ModelAndView getLoginPage(@RequestParam Optional<String> error) {
return new ModelAndView("login", "error", error);
}
}

@PreAuthorize anotasyonuyla, getLoginPage methodunun sadece giriş yapmamış kullanıcılar için invoke edileceğini belirttik. Eğer burada, @RequestParam olarak doğrudan String kullansaydık, ‘error is not present’ hatası alacaktık.

Şimdi ise yukarıda yazdığımız RegisterValidator classını UserController‘da kullanalım.

private final UserService userService;
private final RegisterValidator registerValidator;
@Autowired
public UserController(UserService userService, RegisterValidator registerValidator) {
this.userService = userService;
this.registerValidator = registerValidator;
}
@InitBinder()
public void initBinder(WebDataBinder binder) {
binder.addValidators(registerValidator);
}

Gördüğünüz gibi, az önce yazdığımız validator classını initBinder içerisinde validator olarak ekledik.

config package’ı altında SecurityConfig classımızı oluşturalım:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
@EnableWebMvcSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/register").permitAll()
.antMatchers("/home").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.usernameParameter("username")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}

configure methodu URL bazlı güvenliğin ayarlandığı kısım. Burada register ve home sayfaları hariç bütün sayfaları, giriş yapmamış kullanıcılara kapatıyoruz. login ve logout adreslerini de yine bu methodda belirtiyoruz.

View kısmına geçelim ve uygulamamızı tamamlayalım.

View

Yine resources/templates altında login.html sayfasını oluşturalım:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div sec:authorize="isAuthenticated()">
<meta http-equiv="refresh" content="0;url=/home" />
</div>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>

Burada değineceğim iki nokta var. sec:authorize=”isAuthenticated()” diyerek sadece giriş yapmış kullanıcılara özel içerik sunabiliyoruz. Bunu, yukarıda eklendiğimiz thymeleaf-extras-springsecurity3 dependencysi sayesinde kullanabiliyoruz. Artık, ana sayfamızı şu şekilde değiştirebiliriz:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Inventory Home</title>
</head>
<body>
<nav role="navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/users">Users</a></li>
<li><a href="/items">Items</a></li>
<li sec:authorize="isAuthenticated()">
<a th:href="@{/users/} + ${user.id} + @{/items}">My Items</a>
</li>
</ul>
</nav>

<div sec:authorize="isAnonymous()">
<h1>Welcome!</h1>
</div>

<div sec:authorize="isAuthenticated()">
<h1 th:text="'Welcome, ' + ${user.username} + '!'"></h1>
</div>

<div sec:authorize="isAnonymous()">
<p>Click <a th:href="@{/login}">here</a> to login.</p>
<p>Click <a th:href="@{/register}">here</a> to register.</p>
</div>
<div sec:authorize="isAuthenticated()">
<form th:action="@{/logout}" method="post">
<input type="submit" value="Log Out"/>
</form>
</div>
</body>
</html>

Gördüğünüz gibi, isAuthenticated ve isAnonymous sayesinde giriş yapmış kullanıcılara ve giriş yapmayan ziyaretçilere farklı içerik sunabiliyoruz. Tabii, Welcome, {username}! yazdırmak şu aşamada mümkün değil. HomeController’ı aşağıdaki şekilde değiştirelim:

@Controller
public class HomeController {
@RequestMapping("/")
public ModelAndView getHomePage(@AuthenticationPrincipal User user) {
return new ModelAndView("home", "user", user);
}
}

@AuthenticationPrincipal ile user‘ın o an sisteme giriş yapmış kullanıcı olduğunu belirtiyor ve bu user‘ı ana sayfada kullanacağımız model olarak döndürüyoruz.