Yeni Başlayanlar için Spring Boot MVC ve PostgreSQL ile Proje Oluşturma

Herkese Merhaba, öncelikle Spring Boot nedir, ne işe yaramaktadır bununla başlayalım. Kendi sitesindeki tanımına göre Spring Boot, bize uygulama oluşturmanın hızlı bir yolunu sunar. Sınıf yolumuza ve yapılandırdığımız çekirdeklere bakar, neyi kaçırdığımız hakkında makul varsayımlar yapar ve bu öğeleri ekler. Spring Boot ile iş özelliklerine daha çok, altyapıya daha az odaklanabilmekteyiz. Spring Boot, standalone uygulamalar için gerekli tüm .jar’ları kendi içinde barındırır ve platformdan bağımsız uygulamayı hızlı bir şekilde ayağa kaldırabilir.
MVC (Model-View-Controller), yazdığımız uygulamanın iş mantığı (business logic) ile kullanıcı arayüzünü birbirinden ayrıştıran, uygulamanın farklı amaçlara hizmet eden kısımlarının birbirine girmesini engelleyen yazılım mimarisidir. Basitçe açıklayacak olursak; Model, bir program tarafından kullanılan verilerdir. Bu bir veritabanı, dosya veya bir video oyunundaki bir simge veya karakter gibi basit bir nesne olabilir. View, bir uygulama içindeki nesneleri görüntüleme aracıdır. Kullanıcının görebileceği her şeyi içerir. Controller, hem modelleri hem de görünümleri günceller. Girişi kabul eder ve ilgili güncellemeyi gerçekleştirir.
Ben Spring Boot ile basic bir HelpDesk Projesi oluşturdum. Bu yazımda size projemin önemli kısımlarını anlatacağım.
Projenin bütün kaynak kodlarına buradan ulaşabilirsiniz: https://github.com/minnela/HelpDeskSystem
Spring Boot’un mantığını daha iyi kavramamız açısından bu projede back-end tarafına yoğunlaşacağız. Projemin temel mantığından biraz bahsedecek olursam, yardım masası sistemine kayıt olan kullanıcılar sisteme giriş yaparak şikayetlerini şikayet formu (issue form) doldurarak oluştururlar. Sistemin adminleri bütün herkesin şikayet ve sorunlarını görebilmekte ve kişilerin sorunlarına yorum yazabilmekteler. Projede kullandığımız en önemli özelliklerden ikisi sizin de anlayacağınız üzerine Authorization ve Authentication.
Projemize başlamadan önce gerekliliklerimiz:
PostgreSQL 12,
JDK (en az 11(2),
Maven kullanabilmek için bir IDE ( Ben IntelliJ Idea kullandım.)
Kendi işletim sisteminize uygun PostgreSQL’i burdan indirebilirsiniz: https://www.postgresql.org/download/
Bütün gerekliliklerimizi kurduktan sonra yeni bir proje oluşturmaya başlayabiliriz. IntelliJ Idea’ da File>New>Project > Maven seçerek yeni bir Maven projesi oluşturuyoruz.
Projemizin structure’ı aşağıdaki gibi olacak:

pom.xml dosyasını şu şekilde güncelliyoruz: (thymeleaf dependency, view katmanında kullanacağımız bir template engine’dir)
<?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.minnela</groupId>
<artifactId>issue</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>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.5</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<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>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
…
Spring Boot ile İlk Proje
Database’imizi kurmadan önce basit bir Home sayfası yapalım. Controller package’i altında HomeController adlı bir class oluşturalım.
HomeController:
package com.minnela.issue.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HomeController {
@RequestMapping(value = {"/", "/home"})
public String getHomePage(){
return "home";
}
}
HomeController classımızı Spring Boot’un @Controller anotasyonu ile belirtiyoruz. Burada @RequestMapping anotasyonu ile Home anasayfamıza erişim sağlamak için gerekli url’leri belirliyoruz. Spring Boot default olarak localhost:8080 port adresinde çalışmaktadır. Bu adresi ben localhost:1236 olarak değiştirdim. Siz de başka bir port adresi belirleyebilirsiniz. Adresi değiştirmek için application.properties dosyasına girerek server.port= 1236 yazmamız yeterli olacaktır. Home Controller’dan anlayacağımız üzere localhost:1236/ veya localhost:1236/home adresleri bizi anasayfaya götürecek. Burada return”home” olarak belirttiğimiz ifade bizim home.html sayfamız olacaktır. Anasayfaya istek attığımızda bizi home.html sayfasına yönlendirecek. Bunun için resources>templates>home.html adlı bir dosya oluşturalım.
Ben biraz da görsellik olması açısından anasayfaya arka plan resmi ekledim. Ayrıyeten html sayfalarımda bir bootstrap teması kullandım. Ama daha önceden de dediğim gibi front-end tarafından ziyade bu projede back-end tarafına odaklanıyoruz.
home.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>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"/>
<title>Hello, world!</title>
</head>
<body background="https://wallpapercave.com/uwp/uwp91477.jpeg">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/home">Minnela</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" href="/home">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/users">Users <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/issues/add">Add Issue</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/issues">Issues</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Sign up!
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="/login">Sing in</a>
<a class="dropdown-item" href="/register">Register</a>
</div>
</li>
</ul>
</div>
</nav><h1>Welcome Help Desk System!</h1><script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
Ve şimdi geldik Main classımızı oluşturmaya. Proje dizinimizde Application isimli bir class oluşturup üzerine @SpringBootApplication anotasyonunu koyuyoruz.
Application class:
package com.minnela.issue;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class,args);
}
}
İşte şimdi projemiz çalıştırmaya hazır! localhost:1236/' ya istek yaparak anasayfamızı görebiliriz.
…
Issue Ekleme ve Listeleme
Şimdi Issue entity’mizi oluşturalım. Issue, kullanıcıların yardım masasına gireceği bir isteği, sorunu veya yardım isteğini temsil etmektedir. Bunun için domain dizini altında Issue classımızı oluşturalım:
Model
Issue:
@Entity
public class Issue {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="issueid",nullable = false, updatable = false)
private long id;
@Column(name="issuetype", updatable = false)
private String type;
@Column(name="issueurgency", updatable = false)
private String urgency;
@Column(name="issuedefinition", updatable = false)
private String definition;
@Column(name="issueuseremail", updatable = false)
private String userEmail;
@Column(name="issuecomment", updatable = false)
private String issuecomment;
@Size(min=3, max=50)
private String UserName;
@Size(min=3, max=50)
private String UserSurname;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private Users user;
Issue classımızın başına @Entity anotasyonu ekleyerek bu classın bir entity class’ı olduğunu belirtiyoruz. @Column anotasyonu ile issue tablomuzun columnlarını belirliyoruz. Class’a getter setter ve constructorları da ekledikten sonra Issue class’ımız hazır hale gelmektedir.
Şimdi sıra geldi kullanıcıların sisteme ekleyeceği issue’lar için bir issue form , controller ve veritabanı yaratmaya. Projemize PostgreSQL veritabanını entegre etmek için application.properties dosyasına şunları ekleyelim:
application.properties:
spring.jpa.properties.hibernate.dialect =org.hibernate.dialect.PostgreSQL9Dialect
spring.jpa.hibernate.ddl-auto= none
spring.jpa.hibernate.show-sql =true
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=admin
spring.datasource.initialization-mode=always
spring.datasource.initialize=true
spring.datasource.schema=classpath:/data.sql
spring.datasource.continue-on-error=true
Daha sonra resources dizini altında data.sql adlı bir sql dosyası oluşturup issue tablomuzu yaratacağız. data.sql dosyasını oluşturduğunuzda IntelliJ idea sisteminize bir sql entegre etmenizi isteyecek ve bir uyarı verecektir. Çıkan uyarıda Configure data source seçeneğine tıklayarak IntelliJ Idea içine de PostgreSQL kuruyoruz ve böylece database’imizi IDE içine entegre ediyoruz. Entegre edilen database’imizde manuel olarak users ve issue tablolarını oluşturuyoruz. Burada altını çizmek istediğim bir konu, postgreSQL’in default olarak “user” adında bir tablo bulundurmasından dolayı bizim yazacağımız column’larda hata almamak için ben tablomu users ismiyle oluşturdum.
Database’imizi oluşturduktan sonra sıra geldi Model katmanımızı oluşturmaya. Verilerin eklenip, tutulması için repository dizini altında IssueRepository adlı interface’imizi oluşturalım.
IssueRepository:
package com.minnela.issue.repository;
import com.minnela.issue.domain.Issue;
import java.util.List;
public interface IssueRepository {
void addIssue(Issue issue);
List<Issue> getUserIssues();
void deleteIssueById(long id);
Issue getIssueById(long id);
List<Issue> getIssues();
void addIssueComment(Issue issue);
}
IssueRepository interface’ini implemente eden yine repository dizininin altında bulunan IssueRepositoryImpl class’ını oluşturalım. Implementasyon classımızda database’e issue ekleme, userlara ait issueları listeleme gibi özellikler yazacağız. Database’deki issuelara erişebilmek için bir mapper’a ihtiyacımız olacak. Bunun için önce mapper dizini altında IssueRowMapper class’ını oluşturuyoruz.
IssueRowMapper:
package mapper;
import com.minnela.issue.domain.Issue;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class IssueRowMapper implements RowMapper<Issue> {
@Override
public Issue mapRow(ResultSet rs, int arg1) throws SQLException {
Issue issue = new Issue();
issue.setId(rs.getLong("issueId"));
issue.setDefinition(rs.getString("issuedefinition"));
issue.setUrgency(rs.getString("issueurgency"));
issue.setType(rs.getString("issuetype"));
issue.setUserEmail(rs.getString("issueuseremail"));
issue.setIssuecomment(rs.getString("issuecomment"));
return issue;
}
}
IssueRowMapper classını, issueRepositoryImpl classında yazacağımız sql stringleri ile birlikte kullanarak issue tablosundaki istediğimiz column’ları getirmiş olacağız. Şimdi IssueRepositoryImpl classına bir bakalım:
IssueRepositoryImpl:
@Repository
public class IssueRepositoryImpl implements IssueRepository{
private JdbcTemplate jdbc;
private String currentLoginedUser;
@Autowired
public IssueRepositoryImpl(DataSource dataSource) throws SQLException {
this.jdbc = new JdbcTemplate(dataSource);
}
public IssueRepositoryImpl(String currentLoginedUser) {
this.currentLoginedUser = currentLoginedUser;
}
@Override
public void addIssue(Issue issue) {
SimpleJdbcInsert insertIssue = new SimpleJdbcInsert(jdbc).withSchemaName("public").withTableName("issue").usingGeneratedKeyColumns("issueid");
Map<String, Object> parameters = new HashMap<>(2);
parameters.put("issueid", issue.getId());
parameters.put("issueurgency", issue.getUrgency());
parameters.put("issuedefinition", issue.getDefinition());
parameters.put("issueuseremail", getCurrentLoginedUser());
parameters.put("issuetype", issue.getType());
Number id = insertIssue.executeAndReturnKey(parameters);
issue.setId(id.longValue());
insertIssue.execute(parameters);
}
@Override
public List<Issue> getIssues() {
return jdbc.query("select * from issue", new IssueRowMapper());
}
Implementasyon classımıza @Repository anotasyonunu koyuyoruz. Repository, database’de tuttuğumuz dataya erişim objemizdir. Database’de veri işlemleri yapabilmek için Spring Boot’un sql template özelliği olan jdbc template’ini ve DataSource’u oluşturuyoruz. addIssue metodunda jdbc template’in insert özelliğini kullanarak bir map oluşturuyoruz ve database’imize eklenecek olan issue’ları kaydediyoruz. Burada altını çizmek istediğim nokta, kaydedilen issue’ya ait kullanıcı username’i o an login olan username olarak belirlenmektedir. Yani issue tablosunda, issue sahiplerinin de username’leri barındırılmaktadır. Şimdi uygulamaya girip, bir issue formu doldurarak bunu database’e kaydetmek isteyelim.
Service
Öncelikle controller ve model arasında bir köprü görevi gören service katmanımızı oluşturalım. 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. Bunun için service dizini altında bir issueService interface’i ve onu implemente eden issueServiceImpl classını oluşturuyoruz:
IssueService ve IssueServiceImpl:
public interface IssueService {
void addIssue(Issue issue);
List<Issue> getUserIssues();
void deleteIssueById(long id);
Issue getIssueById(long id);
List<Issue> getIssues();
void addIssueComment(Issue issue);
}@Service
public class IssueServiceImpl implements IssueService {
@Resource
IssueRepository issueRepository;
@Autowired
public IssueServiceImpl(IssueRepository issueRepository) {
this.issueRepository = issueRepository;
}
@Override
public void addIssue(Issue issue) {
issueRepository.addIssue(issue);
}
View
Şimdi sıra geldi addIssue html’ini oluşturmaya. Burada issue eklemek için bir formumuz olacak. Aşağıya sadece formu paylaşıyorum, addIssue.html sayfasının bütününe proje kaynak kodundan ulaşabilirsiniz. Formda kullandığımız thymeleafin özelliği th:action ile localhost:1236/issues sayfasına bir post isteği yaptığımızı belirtiyoruz. th:object ile Issue classından türeyen issue nesnesini kullandığımızı belirtiyoruz. th:field ile issue nesnesinin hangi özelliğini kapsadığımızı gösteriyoruz.
addIssue.html:
<form class="needs-validation" th:action="@{/issues}" th:object="${issue}" th:method="post">
<div class="form-row">
<div class="col-md-6 mb-3">
<label for="validationTooltip03">Issue Type</label>
<input type="text" class="form-control" id="validationTooltip03" placeholder="Issue Type" th:field="*{type}"/>
<small th:if="${#fields.hasErrors('type')}" th:errors="*{type}">Item Type Error</small>
<div class="invalid-tooltip">
Please provide a valid city.
</div>
</div>
<div class="col-md-3 mb-3">
<label for="validationTooltip04">Issue Urgency</label>
<input type="text" class="form-control" id="validationTooltip04" placeholder="Issue Urgency" th:field="*{urgency}"/>
<small th:if="${#fields.hasErrors('urgency')}" th:errors="*{urgency}">Item Type Error</small>
<div class="invalid-tooltip">
Please provide a valid state.
</div>
</div>
<div class="col-md-3 mb-3">
<label for="validationTooltip05">Issue Difficulty</label>
<input type="text" class="form-control" id="validationTooltip05" placeholder="Issue Definition" th:field="*{definition}"/>
<small th:if="${#fields.hasErrors('definition')}" th:errors="*{definition}">Item Type Error</small>
<div class="invalid-tooltip">
Please provide a valid zip.
</div>
</div>
</div>
<button class="btn btn-primary" type="submit">Submit form</button>
</form>
Controller
Şimdi geldik controller classını oluşturmaya. Controller dizini altında bir IssueController yaratarak issue ekleme işlemimizi tamamlayalım:
IssueController:
@Controller
public class IssueController {
private IssueService issueService;
private UserService userService;
@Autowired
public IssueController(IssueService issueService, UserService userService) {
this.issueService = issueService;
this.userService = userService;
}
@RequestMapping("/issues/add")
public ModelAndView issueAddPage(){ return new ModelAndView("addIssue", "issue", new Issue()); }
@RequestMapping(value="/issues", method= RequestMethod.POST)
public String handleIssueAdd(@Valid @ModelAttribute("issue") Issue issue, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return "addIssue";
}
issueService.addIssue(issue);
return "redirect:/issues";
}
@RequestMapping(value = "/issues", method = RequestMethod.GET)
public ModelAndView getIssuesPage(){
return new ModelAndView("issues","issues", issueService.getUserIssues());
}
Burada issues/add adresi ile addIssue sayfasına erişiyoruz. Karşımıza sisteme istekte bulunabilmek için bir issue formu çıkıyor. Issue formunu doldurduktan sonra bu bilgileri issue sayfasına post ediyoruz. Post fonksiyonumuz issueService aracılığıyla issue’ları database’e kayıt ediyor ve bizi issues sayfasına yönlendiriyor. Issues sayfası ise bütün issuelarımızın tablo olarak gösterildiği ve sıralandığı sayfa. getIssuesPage() methodunda ModelAndView objesi sırayla (“viewName”, “modelName”, getUserIssue()) değerleri almıştır. issues.html sayfasında gördüğümüz th:each kodu, getIssuesPage() methodumuzdaki issues olarak gönderdiğimiz model ismini kullanır. Böylece bütün issueları tarayarak teker teker columnları sayfaya yazdırabilmekteyiz :
issues.html:
<table>
<tr>
<th></th>
<th>Issue type</th>
<th>Issue urgency</th>
<th>Issue difficulty</th>
<th></th>
</tr>
<tr th:each="issue : ${issues}">
<td>
<form th:action="@{/issues/} + ${issue.id}" th:method="delete">
<input type="submit" value="Delete" name="delete" />
</form>
</td>
<td th:text="${issue.type}">Issue type</td>
<td th:text="${issue.urgency}">Issue urgency</td>
<td th:text="${issue.definition}">Issue difficulty</td>
<td th:text="${issue.userEmail}">User E-mail</td>
<td> <a class="btn btn-success" href="showSolution.html" target="_blank">Show solution</a> </td>
<td>
</td>
</tr>
</table>
…
User Kayıt
Şimdi sisteme user register işlemi ekleyelim. Bunun için öncelikle Users adlı class’ımızı oluşturalım. Spring Boot’un default olarak User isimli class’ı olduğu için classların birbirine karışmaması adına ben classımı Users olarak oluşturdum:
Model
Users:
@Entity
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="userid", nullable = false,updatable = false)
private long id;
@Column(name="userEmail", nullable = false)
private String username;
@Column(name="userpassword", nullable = false)
private String password;
@Column(name="userName")
private String name;
@Column(name="userSurname")
private String surname;
@Column(name= "userrole")
private String userRole;
@OneToMany(mappedBy = "user")
private Set<Issue> issues;
Yine getter,setter methodlarımızı ve constructor’ımızı ekliyoruz. Ardından aynı issue için yaptığımız gibi UserRowMapper classını, UserRepository interface’ini ve UserRepositoryImpl classını oluşturuyoruz:
UserRowMapper:
public class UserRowMapper implements RowMapper<Users> {
@Override
public Users mapRow(ResultSet rs, int rowNum) throws SQLException {
Users user = new Users();
user.setId(rs.getLong("userid"));
user.setName(rs.getString("username"));
user.setSurname(rs.getString("usersurname"));
user.setUsername(rs.getString("useremail"));
user.setPassword(rs.getString("userpassword"));
user.setUserRole(rs.getString("userrolee"));
return user;
}
UserRepository ve UserRepositoryImpl:
Burada user kaydı yaparken, her kayıt olan user’ın rol atamasını “user” olarak belirledik. Sistemin adminlerini veritabanımıza manuel olarak gireceğiz.
public interface UserRepository {
void addUser(Users user);
List<Users> getUsers();
List<String> getUserNames();
Users getUserByUserName(String username);
Users getUserById(long id);
String getRoleByUserId(long id);
}@Repository
public class UserRepositoryImpl implements UserRepository{
private JdbcTemplate jdbc;
private EncryptedPasswordUtils encryptedPasswordUtils;
@Autowired
public UserRepositoryImpl(DataSource dataSource) {
this.jdbc = new JdbcTemplate(dataSource);
}
@Override
public void addUser(Users user) {
user.setUserRole("user");
String encryptedPassword= encryptedPasswordUtils.encrytePassword(user.getPassword());
user.setPassword(encryptedPassword);
SimpleJdbcInsert insertUser = new SimpleJdbcInsert(jdbc).withSchemaName("public").withTableName("users").usingGeneratedKeyColumns("userid");
Map<String, Object> parameters = new HashMap<>(2);
parameters.put("userid", user.getId());
parameters.put("username", user.getName());
parameters.put("usersurname", user.getSurname());
parameters.put("userpassword", user.getPassword());
parameters.put("useremail", user.getUsername());
parameters.put("userrolee", user.getUserRole());
Number id = insertUser.executeAndReturnKey(parameters);
user.setId(id.longValue());
insertUser.execute(parameters);
}
@Override
public List<Users> getUsers() {
return jdbc.query("select * from users",new UserRowMapper());
}
Ayrıca, user kayıt olurken belirlediği passwordu de güvenliği arttırmak için veritabanına encryption yaparak, yani şifreleyerek kaydediyoruz. Encrption yapmak için utils dizininin altında EncryptedPasswordUtils adlı bir class oluşturup şu kodları ekliyoruz:
package com.minnela.issue.utils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class EncryptedPasswordUtils {
// Encryte Password with BCryptPasswordEncoder
public EncryptedPasswordUtils() {
}
public static String encrytePassword(String password) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder.encode(password);
}
}
Service
Ardından controller ve model arasında iletişim kurmak için service dizini altında UserService interface’ini ve UserServiceImpl classını oluşturalım:
UserService ve UserServiceImpl:
public interface UserService {
void addUser(Users user);
List<Users> getUsers();
List<String> getUserNames();
Users getUserByUserName(String username);
Users getUserById(long id);
}@Service
public class UserServiceImpl implements UserService, UserDetailsService {
@Resource
UserRepository userRepository;
IssueRepositoryImpl issueRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository, IssueRepositoryImpl issueRepository) {
this.userRepository = userRepository;
this.issueRepository=issueRepository;
}
@Override
public void addUser(Users user) {
userRepository.addUser(user);
}
@Override
public List<Users> getUsers() {
return userRepository.getUsers();
}
View
Yine aynı addIssue formunda yaptığımız gibi resources dizini altında bir register formu oluşturalım.
register.html:
<form th:action="@{/register}" th:object="${user}" method="post">
<div class="form-row">
<div class="col-md-4 mb-3">
<label for="validationTooltip01">First name</label>
<input type="text" class="form-control" id="validationTooltip01" placeholder="First name" value="Mark" th:field="*{name}"/>
<small th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Item Type Error</small>
<div class="valid-tooltip">
Looks good!
</div>
</div>
<div class="col-md-4 mb-3">
<label for="validationTooltip02">Last name</label>
<input type="text" class="form-control" id="validationTooltip02" placeholder="Last name" value="Otto" th:field="*{surname}"/>
<small th:if="${#fields.hasErrors('surname')}" th:errors="*{surname}">Item Type Error</small>
<div class="valid-tooltip">
Looks good!
</div>
</div>
</div>
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email" th:field="*{username}"/>
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
<small th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Global Error</small>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password" th:field="*{password}"/>
<small th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Password Error</small>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
Controller
Gördüğümüz gibi bu sefer formda user objesini aldık. Artık register işlemi için UserController yaratmaya hazırız:
UserController:
@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 Users());
}
@RequestMapping(value="/register", method= RequestMethod.POST)
public String handleRegisterForm(@Valid @ModelAttribute("user") Users user, BindingResult bindingResult) throws SQLException {
if(bindingResult.hasErrors()){
return "register";
}
userService.addUser(user);
return "redirect:/";
}
Yine issue kayıt etme mantığına benzer şekilde register sayfasına erişmek için bir GET isteği, kayıt olmak için de doldurduğumuz formla birlikte bir POST isteği yaptık.
…
LOGIN İŞLEMİ — AUTHENTICATION & AUTHORIZATION
Veritabanımıza kayıtlı kullanıcılarımızı oluşturduk. Şimdi ise sırada login işlemi var. Login olmak isteyen kullanıcı veritabanında kayıtlı ise authentication işlemi true dönecek ve sisteme giriş sağlanacaktır.
Ö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, login olmak isteyen kullanıcıları doğrulamak için, ikincisine ise thymeleaf’in bir takım login özelliklerini kullanabilmek için ihtiyacımız var.
Öncelikle config dizininin altında WebSecurityConfig adında bir class oluşturuyoruz. Burada password decryption ( şifre çözme) , kullanıcı doğrulama işlemleri yapılmaktadır. Ayrıca authorization işlemi yapıyoruz, yani hangi sayfaya, hangi yetkiye sahip kullanıcıların erişebileceğini belirliyoruz.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// Setting Service to find User in the database.
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// The pages does not require login
http.authorizeRequests().antMatchers("/","/home", "/login", "/logout").permitAll();
// /userInfo page requires login as ROLE_USER or ROLE_ADMIN.
// If no login, it will redirect to /login page.
http.authorizeRequests().antMatchers("/issues").access("hasAnyRole('user', 'ADMIN')");
http.authorizeRequests().antMatchers("/users").access("hasAnyRole('user','ADMIN')");
http.authorizeRequests().antMatchers("/issues/add").access("hasAnyRole('user', 'ADMIN')");
http.authorizeRequests().antMatchers("/requestSolutionPage").access("hasAnyRole('ADMIN')");
// For ADMIN only.
http.authorizeRequests().antMatchers("/admin").access("hasRole('ADMIN')");
// When the user has logged in as XX.
// But access a page that requires role YY,
// AccessDeniedException will be thrown.
http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/403");
// Config for Login Form
http.authorizeRequests().and().formLogin()//
// Submit URL of login page.
.loginProcessingUrl("/j_spring_security_check") // Submit URL
.loginPage("/login")//
.defaultSuccessUrl("/home")//
.failureUrl("/login?error=true")//
.usernameParameter("username")//
.passwordParameter("password")
// Config for Logout Page
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/logoutSuccessful");
}
}
Ardından UserServiceImpl classımızda UserServiceDetails interface’ini implemente ediyoruz. UserDetailsService, Spring Security’nin user girişini (user password, user yetkileri gibi ) loadUserByUsername methoduyla kontrol ettiği bir interfacedir.
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
Users appUser = this.userRepository.getUserByUserName(userName);
appUser.getPassword();
if (appUser == null) {
System.out.println("User not found! " + userName);
throw new UsernameNotFoundException("User " + userName + " was not found in the database");
}
issueRepository.setCurrentLoginedUser(appUser.getUsername());
System.out.println("Found User: " + appUser);
// [ROLE_USER, ROLE_ADMIN,..]
String roleName = this.userRepository.getRoleByUserId(appUser.getId());
List<GrantedAuthority> grantList = new ArrayList<GrantedAuthority>();
if (roleName != null) {
// ROLE_USER, ROLE_ADMIN,..
GrantedAuthority authority = new SimpleGrantedAuthority(roleName);
grantList.add(authority);
}
UserDetails userDetails = (UserDetails) new User(appUser.getUsername(), //
appUser.getPassword(), true,true,true,true,grantList);
return userDetails;
}
grantList ile kullanıcının yetkilerini bir listede tutuyoruz ve userDetails nesnesine veriyoruz.
Ayrıca o an login olan kullanıcıyı sistemde tutabilmek için ve ekrana kullanıcıya özel issue sayfası getirebilmek için issueRepository’de CurrentLoginedUser fonksiyonu oluşturdum ve bunu loadUserByUsername işleminde o an login olan kullanıcı ismiyle set ettim.
Login Controller
Şimdi ise Controller dizini altında LoginController class’ı oluşturalım. Spring Boot’da login işlemi yaparken post methoduna ihtiyacımız yoktur. Login için controllerda sadece get methodu yazmamız yeterlidir. Bu da Spring Boot’un bize kolaylık sağladığı özelliklerinden biri.
import java.util.Optional;
@Controller
public class LoginController {
private IssueService issueService;
@Autowired
public LoginController(IssueService issueService) {
this.issueService = issueService;
}
@PreAuthorize("isAnonymous()")
@RequestMapping(value = "/login")
public ModelAndView getLoginPage(@RequestParam Optional<String> error) {
return new ModelAndView("loginPage", "error", error);
}
@PreAuthorize anotasyonuyla, getLoginPage methodunun sadece giriş yapmamış kullanıcılar için getirileceğini belirtiyoruz.
View
Login sayfamız için loginPage.html oluşturuyoruz:
<h1>Login</h1>
<h3>Enter user name and password:</h3>
<form name='f' th:action="@{/j_spring_security_check}" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''/></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td><input name="submit" type="submit" value="submit" /></td>
</tr>
</table>
</form>
…
Kullanıcıya Özel Issue Sayfası Listeleme
Her kullanıcı sisteme login olduktan sonra sorunlarını help deske yazabilmek için Add Issue sayfasına gitmekte ve istek yaptığı issue’ların listesini görebilmektedir. Ayrıca her issue’nun yanında Show Solution butonu bulunmakta ve böylece sitem adminlerinin yazmış olduğu çözümleri de görebilmektedir. Sadece tek bir issue viewımız var, fakat biz her kullanıcının sadece kendi eklediği issue’ları görebilmesini istiyoruz. ADMIN yetkisi olan kullanıcının ise bütün issue’ları görebilmesini istiyoruz. Öncelikle issues.html sayfamızın form kısmına tekrar bakalım:
issues.html
<table>
<tr>
<th></th>
<th>Issue type</th>
<th>Issue urgency</th>
<th>Issue difficulty</th>
<th></th>
</tr>
<tr th:each="issue : ${issues}">
<td>
<form th:action="@{/issues/} + ${issue.id}" th:method="delete">
<input type="submit" value="Delete" name="delete" />
</form>
</td>
<td th:text="${issue.type}">Issue type</td>
<td th:text="${issue.urgency}">Issue urgency</td>
<td th:text="${issue.definition}">Issue difficulty</td>
<td th:text="${issue.userEmail}">User E-mail</td>
<td> <a class="btn btn-success" href="showSolution.html" target="_blank">Show solution</a> </td>
<td>
</td>
</tr>
</table>
Şimdi IssueRepositoryImpl classına giderek getUserIssues() adlı methodu oluşturuyoruz (Önce IssueRepository interface’inde methodu oluşturup, burada implemente etmeyi unutmuyoruz):
@Override
public List<Issue> getUserIssues() {
return jdbc.query("select * from issue where issueuseremail = ?", new IssueRowMapper(),getCurrentLoginedUser());}
getUserIssues() methodunda, o an login olmuş olan user’ın username’i çağrılarak kendisine ait olan issue döndürülmektedir.
Şimdi ise IssueController’da da bunu belirtelim:
@RequestMapping(value = "/issues", method = RequestMethod.GET)
public ModelAndView getIssuesPage(){
return new ModelAndView("issues","issues", issueService.getUserIssues());
}
Evet görüldüğü üzere artık user’a özel bir issue sayfası görünümü yarattık. Şimdi ise localhost:1236/admin url’i ile oluşturacağımız admin sayfasına bağlanalım ve sistemde oluşturulan bütün issue’ları görüp onlara comment ekleyebilme özelliğini oluşturalım.
Bunun için önce IssueRepositoryImpl classına veritabanındaki bütün issueları listeleyen bir method yazalım:
@Override
public List<Issue> getIssues() {
return jdbc.query("select * from issue", new IssueRowMapper());
}
Şimdi ise IssueController’da admin sayfası için bir fonksiyon oluşturalım:
@RequestMapping(value = "/admin", method = RequestMethod.GET)
public ModelAndView adminPage(){
return new ModelAndView("adminPage","issues", issueService.getIssues());
}
Viewımızı oluşturalım: AdminPage.html:
<table>
<tr>
<th></th>
<th>Issue type</th>
<th>Issue urgency</th>
<th>Issue difficulty</th>
<th></th>
</tr>
<tr th:each="issue : ${issues}">
<td>
<form th:action="@{/admin/} + ${issue.id}" th:method="delete">
<input type="submit" value="Delete" name="delete" />
</form>
</td>
<td th:text="${issue.id}">Issue Id</td>
<td th:text="${issue.type}">Issue type</td>
<td th:text="${issue.urgency}">Issue urgency</td>
<td th:text="${issue.definition}">Issue definition</td>
<td th:text="${issue.userEmail}">User E-mail</td>
<td> <a class="btn btn-success" href="requestSolutionPage.html" target="_blank">Write a comment</a> </td>
</tr>
</table>
Şuanda admin, veritabanına kayıtlı bütün issueları görebilmektedir. Her bir issue’nun sonunda write a comment butonu ekledik. Böylece admin gönderilen issue’lara çözüm yazacak ve kullanıcının ekranına gönderecektir. Write a comment butonuna basan admin için requestSolutionPage sayfası açılır.
View — requestSolutionPage.html
<form th:action="@{/showSolution}" th:object="${issue}" th:method="post">
<div class="form-group">
<label for="formGroupExampleInput">Enter issue id</label>
<input type="text" class="form-control" id="formGroupExampleInput" placeholder="Enter issue id" th:field="*{id}"/>
</div>
<div class="form-group">
<label for="exampleFormControlTextarea1">Add Issue Comment</label>
<textarea class="form-control" id="exampleFormControlTextarea1" rows="3" th:field="*{issuecomment}"></textarea>
</div>
<button class="btn btn-primary" type="submit">Submit form</button>
</form>
IssueController’da- getRequestSolutionPage fonksiyonu:
@RequestMapping(value="/requestSolutionPage", method = RequestMethod.GET)
public ModelAndView getRequestSolutionPage(){
return new ModelAndView("requestSolutionPage", "issue", new Issue());
}
getRequestSolutionPage View’dan da anlaşılacağı üzere admin gönderilen issue için bir comment yazar ve submit butonuna basar. Submit edilen comment tıpkı issue eklerken yaptığımız gibi bir post methoduyla yeni bir sayfaya gönderilir, veritabanındaki issue tablosunda issueComment sütununa eklenir ve çözüme kavuşturulan issue’ların hepsi gösterilir.
Kullanıcıya özel çözülmüş issue sayfası için issueRepositoryImpl classında şu methodu yazarız:
@Override
public void addIssueComment(Issue issue) {
jdbc.update("update issue set issuecomment = ? where issueid =? ", issue.getIssuecomment(), issue.getId());
}
Bu method, admin tarafından çözüme kavuşturulan her issue’nun tablosunu güncellemektedir.
Kullanıcı kendi issue’sunun çözümünü görmek için show solution butonuna tıklar ve çözüme kavuşmuş ve böylece update edilen issue sayfası önüne gelmektedir. Bunun için showSolution sayfasına admin tarafından yapılan bir post isteği ve user tarafından yapılan bir de get isteği bulunmaktadır.
ShowSolution- Controller (IssueController classı içinde)
@RequestMapping(value="/showSolution", method= RequestMethod.POST)
public String handleShowSolutionPage(@Valid @ModelAttribute("issue") Issue issue, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return "requestSolutionPage";
}
issueService.addIssueComment(issue);
return "redirect:/showAdminSolution";
}
@RequestMapping("/showSolution")
public ModelAndView getShowSolutionPage(){
return new ModelAndView("showSolution","issues", issueService.getUserIssues());
}
View- showSolution.html:
<table>
<tr>
<th></th>
<th>Issue type</th>
<th> </th>
<th>Issue urgency</th>
<th> </th>
<th>Issue definition</th>
<th> </th>
<th>Issue Username</th>
<th> </th>
<th>Issue Solution</th>
<th> </th>
</tr>
<tr th:each="issue : ${issues}">
<td>
<form th:action="@{/issues/} + ${issue.id}" th:method="delete">
<input type="submit" value="Delete" name="delete" />
</form>
</td>
<td th:text="${issue.type}">Issue type</td>
<td> </td>
<td th:text="${issue.urgency}">Issue urgency</td>
<td> </td>
<td th:text="${issue.definition}">Issue difficulty</td>
<td> </td>
<td th:text="${issue.userEmail}">User E-mail</td>
<td> </td>
<td th:text="${issue.issuecomment}">Issue Solution</td>
<td>
</td>
</tr>
</table>
Ardından yine aynı mantıkla adminin de bütün issueların çözümlerini görebilmesi için bir showSolutionAdmin sayfası ekledim.
Böylece sizlere Spring Boot projemin en önemli hatlarını anlatmış bulunmaktayım. Daha ayrıntılı bilgi için siz de projenin kaynak kodlarını inceleyebilirsiniz.
…
Test Driven Development
Son olarak TDD olarak adlandırdığımız Test Driven Development yöntemi, günümüzde kod yazmada çok önemli bir yer haline gelmiştir. Spring Boot projemiz için sizlere örnek bir test classı da ekleyeceğim. Testimizde UserService’i test etmek amacıyla geçici Mock nesneleri oluşturuyoruz (UserRepository, IssueRepository). Ardından setUp kısmında bir user oluşturup set ediyoruz. Sonunda findUserByEmail test methoduyla userService’in getUserByUsername() methodunu test ediyoruz. Ve testimizi tamamlıyoruz.
import com.minnela.issue.domain.Users;
import com.minnela.issue.repository.IssueRepositoryImpl;
import com.minnela.issue.repository.UserRepository;
import com.minnela.issue.service.UserServiceImpl;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyString;
import static org.mockito.MockitoAnnotations.initMocks;
public class UserServiceTest {
@Mock
private UserRepository mockUserRepository;
@Mock
private IssueRepositoryImpl mockIssueRepository;
private UserServiceImpl userServiceUnderTest;
private Users user;
@Before
public void setUp(){
initMocks(this);
userServiceUnderTest = new UserServiceImpl(mockUserRepository,mockIssueRepository);
user.setId(10);
user.setName("luna");
user.setSurname("cat");
user.setUserRole("user");
user.setPassword("34567");
user.setUsername("luna@gmail.com");
Mockito.when(mockUserRepository.getUserByUserName(anyString()))
.thenReturn(user);
}
@Test
public void testFindUserByEmail() {
// Setup
final String email = "luna@gmail.com";
// Run the test
final Users result = userServiceUnderTest.getUserByUserName(email);
// Verify the results
assertEquals(email, result.getUsername());
}
}
İlk blog yazımı okuduğunuz için teşekkür ederim.