3.1 Лабораторийн зорилго
Энэ лабораторид та:
- Spring Boot Web + Thymeleaf төсөл үүсгэх
- Full CRUD (Оюутан) бүтээх
- Form Validation нэмэх
- Session-аар Login/Logout хийх
- Filter/Interceptor-аар Auth шалгах
Хэл: Java 17+ / Spring Boot 3.x | Template: Thymeleaf | DB: H2 (in-memory)
3.2 Лаб 1: Төслийн бүтэц үүсгэх
Алхам 1: Dependencies (pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Алхам 2: application.yml
server:
port: 8080
servlet:
session:
timeout: 30m
spring:
datasource:
url: jdbc:h2:mem:studentdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true # http://localhost:8080/h2-console
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
thymeleaf:
cache: false # Хөгжүүлэлтэд cache унтраах
Алхам 3: Entity
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private Double gpa;
// Constructors
public Student() {}
public Student(String name, String email, Double gpa) {
this.name = name;
this.email = email;
this.gpa = gpa;
}
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Double getGpa() { return gpa; }
public void setGpa(Double gpa) { this.gpa = gpa; }
}
Алхам 4: Repository
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
boolean existsByEmail(String email);
List<Student> findByNameContainingIgnoreCase(String name);
}
3.3 Лаб 2: Service + Form + Controller (Full CRUD)
Алхам 1: StudentForm (Validation)
public class StudentForm {
private Long id;
@NotBlank(message = "Нэр хоосон байж болохгүй")
@Size(min = 2, max = 50, message = "Нэр 2-50 тэмдэгт байх ёстой")
private String name;
@NotBlank(message = "Email хоосон байж болохгүй")
@Email(message = "Email формат буруу байна")
private String email;
@NotNull(message = "GPA оруулна уу")
@DecimalMin(value = "0.0", message = "GPA 0.0-аас багагүй")
@DecimalMax(value = "4.0", message = "GPA 4.0-аас ихгүй")
private Double gpa;
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Double getGpa() { return gpa; }
public void setGpa(Double gpa) { this.gpa = gpa; }
}
Алхам 2: StudentService
@Service
public class StudentService {
private final StudentRepository repository;
public StudentService(StudentRepository repository) {
this.repository = repository;
}
public List<Student> findAll() {
return repository.findAll();
}
public Student findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new StudentNotFoundException("Оюутан олдсонгүй: ID=" + id));
}
public Student create(StudentForm form) {
Student student = new Student(form.getName(), form.getEmail(), form.getGpa());
return repository.save(student);
}
public Student update(Long id, StudentForm form) {
Student student = findById(id);
student.setName(form.getName());
student.setEmail(form.getEmail());
student.setGpa(form.getGpa());
return repository.save(student);
}
public void delete(Long id) {
Student student = findById(id);
repository.delete(student);
}
public StudentForm toForm(Student student) {
StudentForm form = new StudentForm();
form.setId(student.getId());
form.setName(student.getName());
form.setEmail(student.getEmail());
form.setGpa(student.getGpa());
return form;
}
public List<Student> search(String keyword) {
return repository.findByNameContainingIgnoreCase(keyword);
}
}
Алхам 3: StudentController (Full CRUD)
@Controller
@RequestMapping("/students")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
// LIST — Жагсаалт
@GetMapping
public String list(@RequestParam(required = false) String search, Model model) {
List<Student> students;
if (search != null && !search.isBlank()) {
students = studentService.search(search);
model.addAttribute("search", search);
} else {
students = studentService.findAll();
}
model.addAttribute("students", students);
return "students/list";
}
// NEW — Form харуулах
@GetMapping("/new")
public String showCreateForm(Model model) {
model.addAttribute("student", new StudentForm());
model.addAttribute("editMode", false);
return "students/form";
}
// CREATE — Хадгалах
@PostMapping
public String create(@Valid @ModelAttribute("student") StudentForm form,
BindingResult result, Model model,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
model.addAttribute("editMode", false);
return "students/form";
}
studentService.create(form);
redirectAttributes.addFlashAttribute("message", "Оюутан амжилттай нэмэгдлээ!");
return "redirect:/students";
}
// EDIT — Form + одоогийн утга
@GetMapping("/edit/{id}")
public String showEditForm(@PathVariable Long id, Model model) {
Student student = studentService.findById(id);
model.addAttribute("student", studentService.toForm(student));
model.addAttribute("editMode", true);
return "students/form";
}
// UPDATE — Шинэчлэх
@PostMapping("/edit/{id}")
public String update(@PathVariable Long id,
@Valid @ModelAttribute("student") StudentForm form,
BindingResult result, Model model,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
model.addAttribute("editMode", true);
return "students/form";
}
studentService.update(id, form);
redirectAttributes.addFlashAttribute("message", "Оюутан амжилттай шинэчлэгдлээ!");
return "redirect:/students";
}
// DELETE — Устгах
@GetMapping("/delete/{id}")
public String delete(@PathVariable Long id, RedirectAttributes redirectAttributes) {
studentService.delete(id);
redirectAttributes.addFlashAttribute("message", "Оюутан амжилттай устгагдлаа!");
return "redirect:/students";
}
}
3.4 Лаб 3: Thymeleaf Templates
templates/students/list.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Оюутнууд</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
tr:hover { background-color: #f5f5f5; }
.alert { padding: 10px; margin: 10px 0; border-radius: 4px; }
.alert-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.btn { padding: 6px 12px; text-decoration: none; border-radius: 4px; }
.btn-primary { background-color: #007bff; color: white; }
.btn-warning { background-color: #ffc107; color: black; }
.btn-danger { background-color: #dc3545; color: white; }
.search-box { margin: 10px 0; }
</style>
</head>
<body>
<h1>Оюутнуудын жагсаалт</h1>
<!-- Амжилтын мэдэгдэл -->
<div th:if="${message}" class="alert alert-success" th:text="${message}"></div>
<!-- Хайлт -->
<div class="search-box">
<form th:action="@{/students}" method="GET">
<input type="text" name="search" placeholder="Нэрээр хайх..."
th:value="${search}">
<button type="submit" class="btn btn-primary">Хайх</button>
<a th:href="@{/students}" class="btn">Цэвэрлэх</a>
</form>
</div>
<a th:href="@{/students/new}" class="btn btn-primary">+ Шинэ оюутан</a>
<table th:if="${!#lists.isEmpty(students)}" style="margin-top: 10px;">
<thead>
<tr>
<th>ID</th>
<th>Нэр</th>
<th>Email</th>
<th>GPA</th>
<th>Үйлдэл</th>
</tr>
</thead>
<tbody>
<tr th:each="student : ${students}">
<td th:text="${student.id}"></td>
<td th:text="${student.name}"></td>
<td th:text="${student.email}"></td>
<td th:text="${student.gpa}"></td>
<td>
<a th:href="@{/students/edit/{id}(id=${student.id})}" class="btn btn-warning">Засах</a>
<a th:href="@{/students/delete/{id}(id=${student.id})}" class="btn btn-danger"
onclick="return confirm('Устгахдаа итгэлтэй байна уу?')">Устгах</a>
</td>
</tr>
</tbody>
</table>
<p th:if="${#lists.isEmpty(students)}">Оюутан бүртгэгдээгүй байна.</p>
<p style="margin-top: 20px;">Нийт: <strong th:text="${students.size()}">0</strong> оюутан</p>
</body>
</html>
templates/students/form.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${editMode} ? 'Оюутан засах' : 'Шинэ оюутан'">Form</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.form-group { margin: 10px 0; }
label { display: block; font-weight: bold; margin-bottom: 4px; }
input { padding: 8px; width: 300px; border: 1px solid #ccc; border-radius: 4px; }
input.is-invalid { border-color: #dc3545; }
.error { color: #dc3545; font-size: 0.85em; }
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
.btn-primary { background-color: #007bff; color: white; }
.btn-secondary { background-color: #6c757d; color: white; text-decoration: none; padding: 8px 16px; border-radius: 4px; }
</style>
</head>
<body>
<h1 th:text="${editMode} ? 'Оюутан засах' : 'Шинэ оюутан нэмэх'">Form</h1>
<form th:action="${editMode} ? @{/students/edit/{id}(id=${student.id})} : @{/students}"
th:object="${student}" method="POST">
<div class="form-group">
<label for="name">Нэр:</label>
<input type="text" id="name" th:field="*{name}"
th:classappend="${#fields.hasErrors('name')} ? 'is-invalid'">
<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error"></div>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" th:field="*{email}"
th:classappend="${#fields.hasErrors('email')} ? 'is-invalid'">
<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error"></div>
</div>
<div class="form-group">
<label for="gpa">GPA:</label>
<input type="number" step="0.1" id="gpa" th:field="*{gpa}"
th:classappend="${#fields.hasErrors('gpa')} ? 'is-invalid'">
<div th:if="${#fields.hasErrors('gpa')}" th:errors="*{gpa}" class="error"></div>
</div>
<div class="form-group" style="margin-top: 15px;">
<button type="submit" class="btn btn-primary"
th:text="${editMode} ? 'Шинэчлэх' : 'Нэмэх'">Submit</button>
<a th:href="@{/students}" class="btn-secondary">Буцах</a>
</div>
</form>
</body>
</html>
3.5 Лаб 4: Session Login/Logout
User Entity + Repository
@Entity
@Table(name = "users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String role; // ADMIN, USER
// Constructors, Getters, Setters
public AppUser() {}
public AppUser(String email, String password, String name, String role) {
this.email = email;
this.password = password;
this.name = name;
this.role = role;
}
public Long getId() { return id; }
public String getEmail() { return email; }
public String getPassword() { return password; }
public String getName() { return name; }
public String getRole() { return role; }
}
@Repository
public interface UserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByEmail(String email);
}
AuthController
@Controller
public class AuthController {
private final UserRepository userRepository;
public AuthController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/login")
public String loginPage() {
return "auth/login";
}
@PostMapping("/login")
public String login(@RequestParam String email,
@RequestParam String password,
HttpSession session,
RedirectAttributes redirectAttributes) {
Optional<AppUser> userOpt = userRepository.findByEmail(email);
if (userOpt.isPresent() && userOpt.get().getPassword().equals(password)) {
AppUser user = userOpt.get();
session.setAttribute("currentUser", user);
return "redirect:/students";
}
redirectAttributes.addFlashAttribute("error", "Email эсвэл нууц үг буруу!");
return "redirect:/login";
}
@GetMapping("/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/login";
}
}
templates/auth/login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Нэвтрэх</title>
<style>
body { font-family: Arial; display: flex; justify-content: center; margin-top: 100px; }
.login-box { width: 350px; padding: 30px; border: 1px solid #ddd; border-radius: 8px; }
input { width: 100%; padding: 10px; margin: 8px 0; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; }
button { width: 100%; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
.error { color: #dc3545; margin-bottom: 10px; }
</style>
</head>
<body>
<div class="login-box">
<h2>Нэвтрэх</h2>
<div th:if="${error}" class="error" th:text="${error}"></div>
<form th:action="@{/login}" method="POST">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Нууц үг" required>
<button type="submit">Нэвтрэх</button>
</form>
</div>
</body>
</html>
3.6 Лаб 5: Auth Interceptor + Exception Handler
AuthInterceptor
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("currentUser") == null) {
response.sendRedirect("/login");
return false;
}
return true;
}
}
WebConfig — Interceptor бүртгэх
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login", "/logout", "/css/**", "/js/**", "/h2-console/**");
}
}
StudentNotFoundException + Handler
public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(String message) {
super(message);
}
}
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(StudentNotFoundException.class)
public String handleNotFound(StudentNotFoundException ex, Model model) {
model.addAttribute("error", ex.getMessage());
return "error/404";
}
@ExceptionHandler(Exception.class)
public String handleGeneral(Exception ex, Model model) {
model.addAttribute("error", "Серверийн алдаа: " + ex.getMessage());
return "error/500";
}
}
DataInitializer — Анхны өгөгдөл
@Component
public class DataInitializer implements CommandLineRunner {
private final UserRepository userRepository;
private final StudentRepository studentRepository;
public DataInitializer(UserRepository userRepository, StudentRepository studentRepository) {
this.userRepository = userRepository;
this.studentRepository = studentRepository;
}
@Override
public void run(String... args) {
// Хэрэглэгч
userRepository.save(new AppUser("admin@test.com", "admin123", "Админ", "ADMIN"));
userRepository.save(new AppUser("user@test.com", "user123", "Хэрэглэгч", "USER"));
// Оюутнууд
studentRepository.save(new Student("Бат", "bat@test.com", 3.5));
studentRepository.save(new Student("Сараа", "saraa@test.com", 3.8));
studentRepository.save(new Student("Дорж", "dorj@test.com", 2.9));
}
}
Тест хийх:
# Апп ажиллуулах
./mvnw spring-boot:run
# Browser-д нээх
# http://localhost:8080/login
# Email: admin@test.com, Password: admin123
# Оюутнуудын жагсаалт
# http://localhost:8080/students
# CRUD тест:
# + Шинэ оюутан → Нэмэх → Жагсаалтад харагдана
# Засах → Утга өөрчлөх → Шинэчлэгдэнэ
# Устгах → Confirm → Устгагдана
# Хайх → Нэрээр хайна
# H2 Console
# http://localhost:8080/h2-console
# JDBC URL: jdbc:h2:mem:studentdb