In this article:
➤ What layers can be in Spring
➤ How is a Spring application usually structured?
➤ Which is better, annotations or XML in Spring
➤ What is the difference between REST, RPC, GraphQL and SOAP
➤ What parts can be in a Spring application
➤ How is Spring different from Java EE
➤ What are servlets
➤ How to use the Clean Architecture approach in Spring
➤ What are Event Streaming Concepts
➤ What is Public Key Infrastructure (PKI)
➤ What is REST (Representational State Transfer)
➤ What are the parameters of a RESTful service
➤ What are Model, ModelAndView and ViewResolver?
➤ Explain the principles of Inversion of Control (IoC) and Dependency Injection (DI).
➤ What is CSRF (Cross-Site Request Forgery)
➤ What is Aspect-Oriented Programming (AOP)?
➤ How does Spring process a request and issue a response
➤ What is Project Reactor
➤ What is Mono and Flux
➤ What is Domain-Driven Design (DDD)
➤ What is Servlet
➤ What is Prometheus format
➤ What is Structured concurrency
➤ What is the Happens-Before principle (event precedence)
➤ How is WAR (Web Application Archive) different from JAR (Java Archive)
➤ What is the life cycle of beans in Spring
➤ What is CQRS (Command Query Responsibility Segregation)
➤ What is Event Sourcing
➤ What is Circuit Breaker (Chain Switch)
➤ What is JSON-RPC (Remote Procedure Call)
➤ What is Eventual Consistency (eventual consistency) consistency)
➤ What is Git Registry, GitHub Packages, GitHub Actions, Changelog, Release Notes
➤ What are the main data exchange protocols in Spring
➤ What is Protobuf
➤ What is Javadoc
➤ What is Saga Pattern
➤ What is Rollback Pattern
➤ What is the difference between Rollback Pattern and Saga Pattern
➤ What are the main differences between Maven and Gradle
➤ What does pom.xml consist of
➤ What does build.gradle consist of
➤ What is Supplier
➤ What is the difference between record in Java and data class in Kotlin
➤ What is Java JWT (JSON Web Token)
➤ What is Spring MVC
➤ How to use Lombok in Java
➤ What is Swagger / OpenAPI
➤ How to set up access via OpenVPN
➤ What is Keycloak
➤ What are Testcontainers
➤ What is the difference between Jetty, Netty and Tomcat
➤ What is Netty
➤ What is Nginx
➤ Simple web server example
➤ What is Saga Pattern
Saga Pattern is a design pattern used to manage distributed transactions in microservice architectures. A Saga is a sequence of transactions, each updating data in a single service and publishing an event or message. If any transaction in the sequence fails, the Saga calls a series of compensating transactions to undo the changes made earlier.
Key Features of the Saga Pattern:
Distributed Transaction:
Unlike traditional transactions, which are controlled by a database management system (DBMS) and maintain ACID properties, Saga distributed transactions involve multiple steps, each of which can call different services.
Compensating Transactions:
For each step that can change the state of the system, there is a corresponding compensating transaction that can undo its effects. This is necessary if subsequent steps in the Saga cannot complete successfully.
Sequence Management:
Saga can be implemented in two flavors – sequential and parallel. Sequential execution requires each step to complete before the next one can begin. Parallel execution allows some steps to be executed simultaneously, but still requires that some operations depend on the results of previous steps.
Saga Pattern usage examples:
Order processing:
When processing an order, different services may be involved – an order service, a payment service, and a delivery service. If the payment fails, the order must be cancelled and possibly the reserved item released.
Travel booking:
When booking a flight, a hotel, and a car rental as part of a single trip, each booking is a separate Saga step. If one of the steps fails (for example, the car cannot be booked), the other successful bookings must be cancelled to avoid unnecessary charges to the user.
Types of Saga:
Orchestrated Saga:
A central orchestrator manages the execution and coordination of each transaction and compensation transaction. In the orchestration approach, a central coordinator (called an orchestrator) manages the logic of the Saga, telling each service what actions to perform. The orchestrator is responsible for processing responses from services and deciding on next steps, including initiating compensating actions in case of failures.
Advantages:
It is easier to manage and monitor Saga execution, since all control logic is centralized.
It is easier to ensure consistency and recovery from errors.
Disadvantages:
The orchestrator becomes a single point of failure, which potentially reduces the fault tolerance of the system.
May become a bottleneck as the system scales due to centralization of control.
Choreographed Saga:
Each service in a SAGA listens for events and performs its work autonomously, publishing events to other services. There is no centralized control component in this approach. Instead, each service participating in the Saga knows what steps to take next and when to initiate the next step or compensating action. Services communicate with each other by sending events that can be caught by other services, which causes the next steps to be executed.
Advantages:
Less reliance on a single control component, which increases the system’s resiliency.
More flexible and scalable, as new services can be added with minimal changes to other services.
Disadvantages:
It can be more difficult to track the progress of a Saga, as there is no central point of control.
It is more difficult to ensure data consistency due to the asynchronous nature of communication.
How the Saga Pattern works:
A client initiates an order.
Order Service creates an order and publishes an OrderCreatedEvent.
Payment Service processes the OrderCreatedEvent and attempts to complete the payment.
If the payment is successful, Payment Service publishes a PaymentCompletedEvent.
If the payment is unsuccessful, Payment Service publishes a PaymentFailedEvent.
Inventory Service processes the OrderCreatedEvent and reserves the inventory.
If the reservation is successful, Inventory Service publishes an InventoryReservedEvent.
If the reservation fails, Inventory Service publishes InventoryReservationFailedEvent.
Order Service processes PaymentCompletedEvent, PaymentFailedEvent, InventoryReservedEvent, InventoryReservationFailedEvent events and completes or cancels the order.
import org.springframework.stereotype.Service @Service class Service1 { fun performTransaction1() { // Logic for T1 } fun compensateTransaction1() { // Compensation logic for T1 } }
import org.springframework.stereotype.Service @Service class SagaOrchestrator( private val service1: Service1, private val service2: Service2, private val service3: Service3, private val service4: Service4 ) { fun executeSaga() { try { service1.performTransaction1() service2.performTransaction2() service3.performTransaction3() service4.performTransaction4() } catch (e: Exception) { service4.compensateTransaction4() service3.compensateTransaction3() service2.compensateTransaction2() service1.compensateTransaction1() } } }
➤ What is Rollback Pattern
Rollback Pattern is an approach to handling failures in distributed systems in which if one operation in a sequence of transactions fails, all the operations that were performed are rolled back, returning the system to its original state. Unlike the Saga Pattern, which uses compensating transactions, the Rollback Pattern undoes changes using an explicit rollback.
How the Rollback Pattern works:
Compensating actions:
For each operation that changes the state of the system, a compensating action is defined. This action should undo the changes made by the original operation.
Failure detection:
The system should be able to detect when an operation has failed and initiate compensating actions.
Isolated operations:
Operations should be independent and isolated so that compensating actions do not affect other operations.
Transactionality:
Used in systems where it is necessary to ensure the atomicity of operations. This means that all operations within a single transaction are either executed in full or not executed at all.
State Logging:
Often requires the system to maintain a log of changes or states before a transaction begins, so that it can roll back to a previous state.
Rollback Pattern Works:
A client initiates an order creation.
Order Service creates the order and publishes an OrderCreatedEvent.
Payment Service processes the OrderCreatedEvent and attempts to complete the payment.
If the payment is successful, Payment Service publishes a PaymentCompletedEvent.
If the payment fails, Payment Service publishes a PaymentFailedEvent.
Order Service processes the PaymentFailedEvent and cancels the order by publishing an OrderCancelledEvent.
import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class Service1 { @Transactional fun performTransaction1() { // Logic for T1 } @Transactional fun rollbackTransaction1() { // Rollback logic for T1 } } // Similar for Service3 and Service4
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class TransactionManager( private val service1: Service1, private val service2: Service2, private val service3: Service3, private val service4: Service4 ) { @Transactional fun executeTransactions() { try { service1.performTransaction1() service2.performTransaction2() service3.performTransaction3() service4.performTransaction4() } catch (e: Exception) { service4.rollbackTransaction4() service3.rollbackTransaction3() service2.rollbackTransaction2() service1.rollbackTransaction1() throw e } } }
➤ What is the difference between Rollback Pattern and Saga Pattern
Saga Pattern:
Compensating Transactions:
In the event of an error, compensating transactions are executed to undo the effects of the operations that have already been performed.
Async:
Commonly used in asynchronous distributed systems, such as microservices.
Continuity:
Partial failures can be handled and the system continues to operate.
Rollback Pattern:
Full Rollback:
In the event of an error, all operations that have been performed are fully rolled back, returning the system to its original state.
Synchronic:
More commonly used in synchronous systems and requires a global transaction.
Strong Consistency:
Provides strong data consistency, but can be less flexible and performant.
Applications:
The Rollback Pattern is typically used in the context of a single database or within a single service, where all changes can be rolled back to the initial state if an operation fails.
The Saga Pattern is applicable to distributed systems and microservice architectures where transactions span multiple services and rollback must be accomplished through a series of compensating operations between different services.
State Management:
In the Rollback Pattern, state is managed locally within a single system or component, and it is usually possible to use transaction management mechanisms (such as in a DBMS) to roll back to the original state.
The Saga Pattern requires coordination between multiple services, each managing its own state. Rollback in Saga is organized through compensating transactions, which must be explicitly defined and implemented for each step in the process.
Complexity and Management:
The Rollback Pattern is relatively simple to implement within a single system or application where atomic transactions are supported.
The Saga Pattern requires more complex coordination and error handling logic, since each step of the Saga may require compensating actions in other services.
Fault Tolerance and Scalability:
The Rollback Pattern can be limited within a single system, which reduces its applicability to scalable distributed systems.
The Saga Pattern is designed specifically for distributed and scalable systems, providing capabilities for managing complex business processes and providing fault tolerance at the architectural level.
➤ What are Event Streaming Concepts?
Event Streaming concepts are fundamental to building modern distributed systems and microservice architectures. These concepts allow applications to transmit, receive, and process streams of events in real time. The main concepts and components of event streaming are:
The main concepts of event streaming are:
Event:
A record of a significant state change or action that has occurred in the system. For example, adding a new user, updating an order, or completing a transaction.
An event contains information about what happened, such as the event type, timestamp, and data associated with the event.
Event Stream:
A continuous sequence of events arriving in real time.
The stream can be ordered by timestamps or other criteria.
Producer:
A component or service that generates events and sends them to a stream.
An example would be a service that logs events when new users are added.
Consumer:
A component or service that receives events from a stream and processes them.
An example would be an analytics system that processes and analyzes events from a stream.
Topic:
A logical grouping of events in an event streaming system.
Producers send events to a topic, and consumers subscribe to the topic to receive events.
Message Broker:
A system that manages topics and routes events from producers to consumers.
Examples of message brokers include Apache Kafka, RabbitMQ, and Amazon Kinesis.
Event Processing:
Involves filtering, transforming, aggregating, and other operations on events in real time.
Often used to build streaming analytics applications.
Event Streaming Use Cases
Monitoring and Alerting:
Event streaming is used to monitor systems in real time and send alerts when anomalies or events occur.
Real-time analytics:
Processing and analyzing event streams to obtain real-time analytics. Examples: transaction processing, website click tracking.
Microservice architectures:
Microservices can use event streaming for asynchronous communication and coordination. This helps reduce coupling between microservices and improve system scalability.
➤ What is Eventual Consistency?
Eventual Consistency is a data consistency model used in distributed systems and databases. This model guarantees that if no new updates are made to the data, all copies of the data in the system will eventually become consistent. In other words, all changes to the data will eventually be replicated to all nodes in the system.
The main aspects of eventual consistency are:
Asynchronous data updates:
In a distributed system, data updates may occur asynchronously. This means that after changes are made, the data will not be immediately consistent across all nodes in the system.
Eventual consistency:
If no new updates are made, the system guarantees that all nodes will eventually reach the same state.
Temporary inconsistency tolerance:
Temporary data inconsistency is possible and acceptable. The system may temporarily show old or inconsistent data until all updates have been replicated and applied to all nodes.
Applicability:
Eventual consistency is often used in distributed systems such as distributed databases, caching systems, and replication systems where maintaining high availability and performance is important.
Examples of systems with eventual consistency
NoSQL databases:
Many NoSQL databases such as Cassandra, DynamoDB, Couchbase, MongoDB (in replication mode) use the eventual consistency model to achieve high availability and scalability.
Caching systems:
Caching systems such as Redis (in cluster mode) can use eventual consistency to provide high performance when accessing data.
Messaging systems:
Messaging systems such as Apache Kafka can also use the eventual consistency model to provide high performance and availability.
➤ What is SOLID
SOLID is an acronym for five principles of object-oriented programming and design that help create flexible, scalable, and maintainable software systems. Here is a brief description of each principle and examples of how they are applied in the context of Spring:
Single Responsibility Principle:
A class should have only one reason to change, i.e., it should perform only one task.
@Service public class UserService { @Autowired private UserRepository userRepository; public User findUserById(Long id) { return userRepository.findById(id).orElse(null); } public void saveUser(User user) { userRepository.save(user); } } @Service public class EmailService { public void sendWelcomeEmail(User user) { // Logic for sending a letter } }
Here UserService is responsible only for user operations, and EmailService is responsible for sending emails.
Open/Closed Principle:
Classes should be open for extension, but closed for modification.
public interface DiscountPolicy { double applyDiscount(double price); } @Service public class RegularDiscountPolicy implements DiscountPolicy { @Override public double applyDiscount(double price) { return price * 0.9; // 10% discount } } @Service public class SpecialDiscountPolicy implements DiscountPolicy { @Override public double applyDiscount(double price) { return price * 0.8; // 20% discount } }
The RegularDiscountPolicy and SpecialDiscountPolicy classes can extend behavior without changing existing code.
Liskov Substitution Principle:
Subclass objects should replace base class objects without changing the correctness of the program.
public interface NotificationSender { void sendNotification(String message); } @Service public class EmailNotificationSender implements NotificationSender { @Override public void sendNotification(String message) { // Email sending logic } } @Service public class SmsNotificationSender implements NotificationSender { @Override public void sendNotification(String message) { // SMS sending logic } }
Both the EmailNotificationSender and SmsNotificationSender classes can be used in the context of the NotificationSender interface.
Interface Segregation Principle:
Clients should not depend on interfaces they do not use.
public interface UserRepository { void save(User user); User findById(Long id); } public interface UserDeletionRepository { void delete(User user); } @Service public class UserServiceImpl implements UserRepository, UserDeletionRepository { @Override public void save(User user) { // Logic for saving user } @Override public User findById(Long id) { // User search logic return null; } @Override public void delete(User user) { // Logic for deleting a user } }
Here the interfaces are separated so that each one represents a separate set of functionality.
Dependency Inversion Principle:
Top-level modules should not depend on lower-level modules; both should depend on abstractions.
public interface MessageSender { void sendMessage(String message); } @Service public class SmsMessageSender implements MessageSender { @Override public void sendMessage(String message) { // SMS sending logic } } @Service public class NotificationService { private final MessageSender messageSender; @Autowired public NotificationService(MessageSender messageSender) { this.messageSender = messageSender; } public void sendNotification(String message) { messageSender.sendMessage(message); } }
Here, NotificationService depends on the MessageSender abstraction rather than a concrete implementation, making it easy to replace the implementation if needed.
➤ How to use Clean Architecture approach in Spring
Clean Architecture is a software development concept proposed by Robert C. Martin (Uncle Bob). It separates a system into different layers with clearly defined responsibilities and dependencies, resulting in a flexible, testable, and maintainable system.
The main principles of Clean Architecture are:
Dependencies are inward-facing:
Outer layers can depend on inner layers, but not vice versa.
Outer Layer:
Includes the user interface and infrastructure.
Inner Layer:
Includes business logic and entities.
Input and Output Ports:
Define interfaces for interaction between layers.
Project Structure:
Example of a project structure using Clean Architecture in Spring Boot:
src/main/java/com/example/demo ├── DemoApplication.java ├── config │ └── WebConfig.java ├── controller │ └── UserController.java ├── entity │ └── User.java ├── repository │ └── UserRepository.java ├── service │ └── UserService.java ├── usecase │ └── GetUserUseCase.java └── web ├── UserRequest.java └── UserResponse.java
➤ What are the parameters of a RESTful service?
When developing a RESTful web service, there are several key parameters and aspects to consider to ensure compliance with REST principles and achieve maximum efficiency, usability, and scalability. Here are the main parameters to consider:
Resources:
URL (Uniform Resource Locator): Resources are identified by unique URLs. For example, /users for users or /products for products.
Naming: Resources are usually named in the plural (e.g. /users, /orders).
HTTP Methods:
GET: To retrieve a resource or a list of resources.
POST: To create a new resource.
PUT: To update an existing resource.
DELETE: To delete a resource.
PATCH: To partially update a resource.
HTTP Status Codes:
200 OK: The request was successful.
201 Created: The resource was successfully created.
204 No Content: The request was successful, but no content was returned.
400 Bad Request: Invalid request.
401 Unauthorized: Authentication required.
403 Forbidden: Access denied.
404 Not Found: Resource not found.
500 Internal Server Error: Internal server error.
HTTP Headers:
Content-Type: Specifies the content type (e.g. application/json).
Accept: Specifies what content format the client expects.
Authorization: Used to convey authentication information.
Data Formats:
JSON (JavaScript Object Notation): A widely used format for transmitting data.
XML (eXtensible Markup Language): Used in some systems to transmit data.
Error Handling:
Error messages should be descriptive and provide enough information to understand the problem.
Structured error messages in JSON or XML format.
Caching:
Using caching headers (Cache-Control, ETag, Last-Modified) to improve performance and reduce server load.
Authentication and Authorization:
Basic Auth: A simple authentication method using a username and password.
Token-based Auth (e.g. JWT): A more secure and flexible authentication method.
OAuth: A protocol for authorization that allows third-party applications to gain limited access to resources.
API Versioning:
Including the version in the URL (e.g. /v1/users).
Using headers to indicate the API version.
Documentation:
Documentation should be complete and clear, including a description of all endpoints, parameters, data formats, error statuses, and request/response examples.
Documentation tools: Swagger/OpenAPI.
➤ What is Model, ModelAndView and ViewResolver
The Spring MVC (Model-View-Controller) architecture uses several key components to manage data, views, and their interactions. Among them, Model, ModelAndView, and ViewResolver play an important role. Let’s look at each of these concepts in more detail.
Model:
An interface that is used to pass data from the controller to the view. It allows you to add attributes that will be available in the view template. Model is typically used in controller methods. In this example, the greeting method adds a message attribute to the Model, which will then be available in the view named greeting.
import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping @Controller class MyController { @GetMapping("/greeting") fun greeting(model: Model): String { model.addAttribute("message", "Hello, World!") return "greeting" } }
ModelAndView:
A class that combines a model and a view. It is used to pass both data and view information in a single object. This is useful when you need to return both a model and a view from a controller. In this example, the welcome method creates a ModelAndView instance, sets the view name to welcome, and adds the message attribute.
import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.servlet.ModelAndView @Controller class MyController { @GetMapping("/welcome") fun welcome(): ModelAndView { val modelAndView = ModelAndView("welcome") modelAndView.addObject("message", "Welcome to Spring MVC!") return modelAndView } }
ViewResolver:
An interface that defines a view object based on its name. It is used to map logical view names to specific views. Depending on the settings, ViewResolver can look for views in the file system, in a classpath, or use other mechanisms. This configuration class configures InternalResourceViewResolver, which looks for JSP files in the specified directory.
spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.jsp
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import org.springframework.web.servlet.view.InternalResourceViewResolver @Configuration class WebConfig : WebMvcConfigurer { @Bean fun viewResolver(): InternalResourceViewResolver { val resolver = InternalResourceViewResolver() resolver.setPrefix("/WEB-INF/views/") resolver.setSuffix(".jsp") return resolver } }
➤ What are the principles of Inversion of Control (IoC) and Dependency Injection (DI)
Inversion of Control (IoC):
A programming principle in which the management of objects and their dependencies is delegated to an external container or framework. This means that instead of creating and managing dependencies manually, the programmer lets the container manage these tasks. IoC helps to decouple different parts of a system and reduce the dependencies between them, making the system more flexible and easier to test.
IoC principles:
The container manages the lifecycle of objects: The container is responsible for creating, initializing, and destroying objects.
The container manages the dependencies of objects: The container injects the necessary dependencies into objects.
Dependency Injection (DI):
A process in which the Spring container automatically supplies dependencies (objects that your class depends on) into an object to minimize hard dependencies and improve testability and maintainability of the code.
Key concepts and types of DI in Spring:
IoC Container (Inversion of Control Container):
A container that manages the lifecycle of beans and their dependencies. In Spring, the primary containers are ApplicationContext and BeanFactory.
Beans:
These are objects that are managed by the Spring container. Beans are created, wired, and managed by the container based on metadata configuration.
Annotations:
Annotations are used to tell the Spring container how to inject dependencies. Key annotations include @Component, @Service, @Repository, @Controller, @Autowired, @Qualifier, and @Inject.
Types of Dependency Injection:
Constructor Injection:
Dependencies are passed in through the class constructor.
The preferred method of DI as it makes dependencies explicitly visible and promotes object immutability.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User findUserById(Long id) { return userRepository.findById(id).orElse(null); } }
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @Service class UserService @Autowired constructor( private val userRepository: UserRepository ) { fun findUserById(id: Long): User? { return userRepository.findById(id).orElse(null) } }
Setter Injection:
Dependencies are passed through setters or other class methods.
Useful for optional dependencies and allows easy change of dependencies after object creation.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } public User findUserById(Long id) { return userRepository.findById(id).orElse(null); } }
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @Service class UserService { private lateinit var userRepository: UserRepository @Autowired fun setUserRepository(userRepository: UserRepository) { this.userRepository = userRepository } fun findUserById(id: Long): User? { return userRepository.findById(id).orElse(null) } }
Field Injection:
Dependencies are passed directly to class fields.
Simple implementation method, but less preferred due to testability and manageability disadvantages.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { @Autowired private UserRepository userRepository; public User findUserById(Long id) { return userRepository.findById(id).orElse(null); } }
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @Service class UserService { @Autowired private lateinit var userRepository: UserRepository fun findUserById(id: Long): User? { return userRepository.findById(id).orElse(null) } }
Spring configuration:
Using annotations (Java-based Configuration):
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackages = "com.example") public class AppConfig {}
Using XML configuration:
<!-- applicationContext.xml --> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <context:component-scan base-package="com.example"/> <bean id="userService" class="com.example.UserService"> <property name="userRepository" ref="userRepository"/> </bean> <bean id="userRepository" class="com.example.UserRepository"/> </beans>
➤ What is Domain-Driven Design (DDD)
Domain-Driven Design (DDD) is a software development methodology proposed by Eric Evans in his book “Domain-Driven Design: Tackling Complexity in the Heart of Software”. DDD focuses on creating software systems that accurately reflect complex business domains.
Core Concepts of Domain-Driven Design:
Domain Models:
A domain model is an abstraction of the real world that includes the major entities, their attributes, and relationships. A domain model is developed in close collaboration with domain experts.
Ubiquitous Language:
Ubiquitous Language is a common language used by all project participants, including developers and domain experts. This language helps eliminate ambiguities and improve communication.
Bounded Contexts:
A constraint context is a boundary within which certain terms and concepts have clear meaning. It helps manage complexity by partitioning a system into logical parts.
Entities:
Entities are objects that have a unique identifier and are defined by their identity rather than by attributes.
Value Objects:
Value Objects are objects that are defined by their attributes and do not have a unique identifier. They are immutable and reusable.
Aggregates:
Aggregates are groups of related entities and value objects that are treated as a single unit. Aggregates ensure data integrity and manage changes through the Aggregate Root.
Services:
Services are operations or actions that do not belong to any particular entity or value object, but are important to the domain.
Repositories:
Repositories provide interfaces for accessing entities and aggregates from a data store, abstracting away the implementation details of the store.
Modules:
Modules help organize domain objects and their interactions by dividing the system into logical parts.
➤ What is CQRS (Command Query Responsibility Segregation)
CQRS (Command Query Responsibility Separation) is an architectural pattern that separates read (query) and write (command) operations into separate models. This allows each operation to be optimized separately, improving the performance, scalability, and flexibility of the system.
The main ideas behind CQRS are:
Separation of models:
Command Model:
processes commands, modifies the system state, and applies business logic.
Query Model:
processes requests to read data, optimized for fast and efficient querying.
Commands and Queries:
Command:
an operation that modifies the state of the system (e.g., creating an order, updating a user profile). Commands are typically write operations.
Query:
an operation that retrieves data from the system (e.g., getting a list of orders, getting a user profile). Queries are typically read operations.
➤ What is Event Sourcing
Event Sourcing is an architectural pattern in which the state of a system is modeled as a sequence of events. Instead of storing the current state of an object, the system stores all the events that led to this state. This allows the current state of the system to be reproduced by re-applying all the events.
The main ideas of Event Sourcing:
Events as a source of truth:
All changes to the state of the system are recorded as events. Each event describes a change in the state of the system at a certain point in time.
State Replay:
The current state of an object can be obtained by sequentially applying all the events associated with this object.
Immutability of Events:
Events are immutable and cannot be changed once recorded. This provides a reliable and accurate history of changes to the system.
➤ What is a Circuit Breaker?
Circuit Breaker is a design pattern used to improve the resilience and reliability of distributed systems, especially microservice architectures. The goal of this pattern is to prevent repeated attempts to perform an operation that is likely to fail and to quickly identify failures to minimize their impact on the system.
The main ideas behind Circuit Breaker are:
Preventing cascading failures:
When one service in a system fails or malfunctions, attempts to interact with it can cause the entire system to slow down or even fail. Circuit Breaker helps prevent such cascading failures.
Three states:
Closed:
Requests pass through the Circuit Breaker to the target service. If the number of errors exceeds a specified threshold, the Circuit Breaker moves to the Open state.
Open:
Requests fail immediately without reaching the target service. After a certain timeout, the Circuit Breaker moves to the Half-Open state.
Half-Open:
A certain number of requests are allowed to pass to the target service to check if it is healthy. If the requests are successful, the Circuit Breaker returns to the Closed state. If more errors occur, the Circuit Breaker returns to the Open state.
Fallback mechanism:
If the target service starts working normally, the Circuit Breaker automatically returns to the Closed state, allowing requests to pass again.
Example using the Resilience4j library:
Resilience4j is a popular library for implementing resilience patterns, including Circuit Breaker, in Java applications.
dependencies { implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1' }
resilience4j.circuitbreaker: instances: myService: registerHealthIndicator: true slidingWindowSize: 10 failureRateThreshold: 50 waitDurationInOpenState: 10000 permittedNumberOfCallsInHalfOpenState: 3
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker import org.springframework.stereotype.Service import org.springframework.web.client.RestTemplate @Service class MyService(private val restTemplate: RestTemplate) { @CircuitBreaker(name = "myService", fallbackMethod = "fallback") fun callExternalService(): String { return restTemplate.getForObject("http://example.com/api", String::class.java) ?: "No response" } fun fallback(throwable: Throwable): String { return "Fallback response" } }
@CircuitBreaker:
annotation is used to indicate the method to which the Circuit Breaker is applied.
name = “myService”:
points to the Circuit Breaker configuration defined in application.yml.
fallbackMethod = “fallback”:
points to the method that will be called if the Circuit Breaker is triggered.
➤ What is JSON-RPC (Remote Procedure Call)
JSON-RPC (Remote Procedure Call) is a remote procedure call protocol used to make method calls on a remote server. It uses JSON (JavaScript Object Notation) to encode data and exchange messages between the client and server. JSON-RPC is a lightweight and simple protocol that supports both one-way notifications and two-way calls and responses.
Key features of JSON-RPC:
JSON format:
Messages are encoded in JSON format, making them easy to understand and process.
Requests and responses:
Requests contain the name of the method to be called and the parameters for that method.
Responses contain the result of the method call or error information if the call fails.
Notification support:
Notifications are one-way messages that do not require a response from the server.
Simple and lightweight:
The JSON-RPC protocol is minimalistic and does not contain complex constructs, which simplifies its implementation and use.
Example of using JSON-RPC in a Spring Boot application:
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.googlecode.jsonrpc4j:jsonrpc4j:1.5.0") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") }
import com.googlecode.jsonrpc4j.JsonRpcService @JsonRpcService("/api") interface CalculatorService { fun subtract(a: Int, b: Int): Int }
import org.springframework.stereotype.Service @Service class CalculatorServiceImpl : CalculatorService { override fun subtract(a: Int, b: Int): Int { return a - b } }
import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImplExporter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class JsonRpcConfig { @Bean fun autoJsonRpcServiceImplExporter(): AutoJsonRpcServiceImplExporter { return AutoJsonRpcServiceImplExporter() } }
import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImplExporter import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api") class CalculatorController( private val calculatorService: CalculatorService ) : CalculatorService by calculatorService
➤ What is Spring MVC
The Spring MVC (Model-View-Controller) architecture is based on the well-known MVC design pattern, which separates an application into three main components: model, view, and controller. This pattern helps separate business logic, presentation logic, and control logic, making the application more modular, maintainable, and extensible.
The main components of the Spring MVC architecture are:
Model:
The model represents the application data. It contains the business logic and interacts with the database to retrieve or save data.
In Spring MVC, the model is typically represented by Java objects (POJOs) that are handled by services and repositories.
View:
The view is responsible for displaying data to the user. It generates a user interface based on the model data.
In Spring MVC, views can be represented by various technologies such as JSP, Thymeleaf, Freemarker, and others.
Controller:
The controller receives incoming requests from the client, processes them, and returns the appropriate view.
Controllers in Spring MVC are typically annotated with @Controller and use methods annotated with @RequestMapping, @GetMapping, @PostMapping, etc. to map requests.
The main components of Spring MVC are:
DispatcherServlet:
The main component of Spring MVC that acts as the front controller. It accepts all incoming HTTP requests, routes them to the appropriate handlers, and returns the appropriate HTTP responses.
Handler Mapping:
Responsible for determining which controller and method should handle the incoming request. It uses URL mapping and annotations to map requests to controller methods.
Controllers:
Process requests, execute application logic, and return model data to be used in the view.
View Resolver:
Determines which view should be used to display data. It resolves the logical name of the view to the physical location of the view, for example, resolves the name of the view to the path of a JSP file.
Model and View:
An object that contains model data and information about the view that should be used to display the data.
Request handling flow in Spring MVC:
Receiving a request:
The DispatcherServlet receives an HTTP request.
Defining a controller:
Handler Mapping determines which controller and method should handle the request.
Processing a request:
The controller processes the request, executes business logic, and requests data from the model.
Generating a response:
The controller returns a ModelAndView object or the model data along with the logical name of the view.
Defining a view:
The View Resolver determines which view should be used based on the logical name of the view.
Rendering a view:
The view is rendered using the model data and sent back to the client as an HTTP response.
➤ What layers can be in Spring
In a typical Spring application, layers are divided into several categories: controllers, services, repositories, and models. These layers help structure the code and improve its maintainability. Here are examples of each layer:
Model:
A model represents the data and its structure. In Spring JPA, these are often entities that represent database tables.
import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id @Entity data class User( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, val username: String, val password: String, val role: String )
Repository:
The repository is responsible for accessing data and performing CRUD (create, read, update, delete) operations.
import org.springframework.data.jpa.repository.JpaRepository interface UserRepository : JpaRepository<User, Long> { fun findByUsername(username: String): User? }
Service:
The service contains the business logic of the application. It processes data received from repositories and passes it to controllers.
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service @Service class UserService( private val userRepository: UserRepository, private val passwordEncoder: PasswordEncoder ) { fun findUserByUsername(username: String): User? { return userRepository.findByUsername(username) } fun registerUser(username: String, password: String, role: String): User { val encodedPassword = passwordEncoder.encode(password) val user = User(username = username, password = encodedPassword, role = role) return userRepository.save(user) } }
Controller:
The controller processes HTTP requests and returns responses. It uses services to execute business logic and prepare data for views.
import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PostMapping @Controller class UserController( private val userService: UserService ) { @GetMapping("/register") fun showRegistrationForm(model: Model): String { model.addAttribute("user", User()) return "register" } @PostMapping("/register") fun registerUser(@ModelAttribute user: User): String { userService.registerUser(user.username, user.password, "ROLE_USER") return "redirect:/login" } }
Security Configuration:
Security Configuration is responsible for setting up access and authorization.
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.SecurityFilterChain @Configuration @EnableWebSecurity class SecurityConfig( private val userDetailsService: UserDetailsService ) { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http .authorizeRequests { requests -> requests .antMatchers("/", "/home", "/register").permitAll() .anyRequest().authenticated() } .formLogin { form -> form .loginPage("/login") .permitAll() } .logout { logout -> logout.permitAll() } return http.build() } @Bean fun passwordEncoder(): PasswordEncoder { return BCryptPasswordEncoder() } }
Views:
Views display data to the user. They are often created using templating engines such as Thymeleaf.
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Home Page</title> </head> <body> <h1>Welcome</h1> <div th:if="${#httpServletRequest.remoteUser != null}"> <p>Welcome, <span th:text="${#httpServletRequest.remoteUser}">User</span>!</p> <a href="/logout">Logout</a> </div> <div th:if="${#httpServletRequest.remoteUser == null}"> <a href="/login">Login</a> | <a href="/register">Register</a> </div> </body> </html>
Main class:
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array<String>) { runApplication<DemoApplication>(*args) }
➤ How a Spring application is usually structured
Dividing the application into modules and folders helps to structure the code, improves readability and maintainability. Spring applications usually use structuring by layers and functional modules. Here are several approaches to organizing the project structure:
Structuring by layers:
The most common approach is dividing into layers: controllers, services, repositories and models.
src/main/kotlin/com/example/demo ├── controller │ ├── HomeController.kt │ ├── UserController.kt ├── service │ ├── UserService.kt ├── repository │ ├── UserRepository.kt ├── model │ ├── User.kt ├── security │ ├── SecurityConfig.kt └── DemoApplication.kt
Structuring by Functional Modules:
This approach organizes code by functional areas, which is useful for large projects with many different functional areas.
src/main/kotlin/com/example/demo ├── user │ ├── controller │ │ └── UserController.kt │ ├── service │ │ └── UserService.kt │ ├── repository │ │ └── UserRepository.kt │ └── model │ └── User.kt ├── home │ └── controller │ └── HomeController.kt ├── security │ └── SecurityConfig.kt └── DemoApplication.kt
Structuring by technical layers:
In this approach, classes are grouped by technical layers, which is especially useful for more complex architectures such as multi-layer or microservice architectures.
src/main/kotlin/com/example/demo ├── config │ ├── SecurityConfig.kt ├── controller │ ├── HomeController.kt │ ├── UserController.kt ├── dto │ ├── UserDto.kt ├── exception │ ├── CustomException.kt ├── model │ ├── User.kt ├── repository │ ├── UserRepository.kt ├── service │ ├── UserService.kt ├── util │ ├── PasswordEncoderUtil.kt └── DemoApplication.kt
➤ Which is better, annotations or XML in Spring
There are two main approaches to configuration and dependency management in the Spring Framework: using annotations and using XML configuration. Both approaches have their advantages and disadvantages, and the choice between them depends on the specific requirements of the project and the developer’s preferences.
Annotations in Spring:
Annotations in Spring allow you to configure and manage dependencies directly in the code, which makes the configuration more readable and easier to maintain.
XML Configuration in Spring:
XML configuration allows you to describe beans and their dependencies in a separate XML file, separating the configuration from the code. This makes the configuration more declarative and easy to change without changing the code.
The main elements of XML configuration:
<bean>:
Defines a bean and its properties.
<property>:
Defines the properties of a bean.
<constructor-arg>:
Defines the arguments of the bean constructor.
context:component-scan:
Scans the specified packages for annotated classes.
Example of using XML configuration:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.example.demo"/> <bean id="userService" class="com.example.demo.service.UserService"/> <bean id="userController" class="com.example.demo.controller.UserController"> <property name="userService" ref="userService"/> </bean> </beans>
➤ What is the difference between REST, RPC, GraphQL and SOAP
REST, RPC, GraphQL, and SOAP are different architectural styles and protocols for building web services. Let’s look at the main differences between them.
REST (Representational State Transfer):
Pros:
Simplicity: Uses standard HTTP methods, making it easier to implement and understand.
Caching: Support for caching at the HTTP level improves performance.
Scalability: Easily scales due to resource independence.
Flexibility: Can use various data formats such as JSON, XML, YAML, etc.
Widespread: Supported by most modern web applications and frameworks.
Cons:
Limited functionality: Does not support complex operations compared to other protocols.
Data redundancy: Can transfer more data than the client requires.
Security: Requires additional measures to ensure security, such as OAuth.
RPC (Remote Procedure Call):
Pros:
Call transparency: Easy to use, as remote calls appear to be local.
Convenience: Good for performing complex operations with a single call.
Efficiency: Data is transferred only when a method is called, minimizing redundancy.
Cons:
Tight coupling: The client and server are tightly coupled, making API changes difficult.
Lack of standards: There is no single standard for implementation, which can cause incompatibilities between different systems.
Scalability: Less scalable than REST due to the need to maintain session state.
GraphQL:
Pros:
Flexibility: Clients can request only the data they need.
Single entry point: All requests go through a single endpoint, simplifying the architecture.
Efficiency: Fewer requests, as all the data needed can be retrieved in a single request.
Typing: Strong typing makes it easy to document and validate requests.
Cons:
Complexity: More complex to implement and maintain than REST.
Server Overload: Can cause server overload if the client requests too much data.
Tooling: Requires special tooling and libraries to work with GraphQL.
SOAP (Simple Object Access Protocol):
Pros:
Reliability: Supports WS-Security, WS-ReliableMessaging, and other extensions to ensure security and reliability.
Standards: Has strict standards and specifications, which makes it easier to integrate between different systems.
Functionality: Supports complex operations and transactions.
Cross-platform: Well supported in enterprise environments and often used in integration solutions.
Cons:
Complexity: More complex to implement and understand than REST.
Redundancy: Uses XML to transfer data, resulting in a larger volume of data compared to JSON.
Performance: Slower than REST due to the use of XML and additional overhead.
➤ What parts can be in a Spring application
The main components of Spring are:
Spring Framework:
A core framework that includes an inversion of control (IoC) container, aspect-oriented programming (AOP), transactions, and other core features.
Spring Boot:
Make it easy to create standalone Spring applications with minimal configuration. Includes autoconfiguration, built-in web servers, and a variety of starters.
Web Development:
Spring MVC:
A framework for developing web applications with templating, support for RESTful web services.
Spring WebFlux:
A reactive web framework for creating reactive web applications and microservices.
Thymeleaf:
A server-side templating engine for creating HTML pages.
Angular, React, Vue.JS, TypeScript:
Front-end frameworks for creating client-side applications that can be integrated with Spring Boot via a RESTful API or GraphQL.
Data Access:
Spring Data JPA:
Integration with JPA to simplify working with databases via ORM.
Spring Data MongoDB:
MongoDB support.
Spring Data Redis:
Redis support.
Spring JDBC:
Simple interaction with databases via JDBC.
Spring Data REST:
Automatic creation of REST API for data repositories.
Integration and Messaging:
Spring Integration:
A framework for building integration solutions based on Enterprise Integration Patterns.
Spring Batch:
Support for batch data processing.
Spring AMQP:
RabbitMQ support.
Spring Kafka:
Support for Apache Kafka for messaging.
Security:
Spring Security:
A framework for authentication and authorization of applications.
Spring Security OAuth2:
Support for OAuth2 and OpenID Connect.
Microservices and Cloud:
Spring Cloud:
A toolkit for developing microservices, including configuration, service discovery, routing, and more.
Spring Cloud Netflix:
Integration with Netflix OSS libraries like Eureka, Hystrix, and Zuul.
Spring Cloud Gateway:
A reactive API gateway for routing and request processing.
Spring Cloud Config:
Externalize centralized configuration for microservices.
Docker:
Containerize applications for easy deployment and management.
Kubernetes:
Container orchestration for scalability and reliability.
CI/CD tools:
Jenkins, GitLab CI, CircleCI, and more.
DevOps and Monitoring:
Spring Boot Actuator:
Metrics, monitoring, and application management.
Spring Cloud Sleuth:
Distributed systems tracing.
Spring Boot Admin:
Administrative interface for managing and monitoring Spring Boot applications.
API and Documentation:
Swagger/OpenAPI:
Tools for documenting and testing APIs.
Spring REST Docs:
Generate documentation for REST APIs based on tests.
Monitoring and Management:
Spring Boot Actuator:
A set of tools for monitoring and managing Spring Boot applications.
Prometheus, Grafana, Splunk, Dynatrace:
Monitoring and visualizing metrics.
ELK Stack (Elasticsearch, Logstash, Kibana):
Collecting, storing, and analyzing logs.
Other utilities and tools:
MapStruct:
A tool for mapping DTOs and entities.
Lombok:
A library for reducing boilerplate code (e.g. getters, setters, constructors).
Spring Cache:
A caching abstraction with support for various providers (EhCache, Hazelcast, Redis).
Testing:
Spring Test:
Support for unit and integration testing with JUnit and TestNG.
Spring Boot Test:
Support for testing Spring Boot applications, including web layer and data access testing.
Mockito:
A framework for creating mock objects and writing tests.
Selenium, Cucumber
➤ How Spring differs from Java EE
Spring and Java EE (now Jakarta EE) are two different platforms for creating enterprise applications in Java. Both platforms offer different tools and frameworks for development, but they have their own characteristics, advantages and disadvantages.
The main differences between Spring and Java EE (Jakarta EE):
Architecture and approaches:
Spring:
is a framework that provides comprehensive solutions for application development. It is modular and allows you to use only the necessary components.
Uses a container for inversion of control (IoC) and dependency injection (DI).
Lightweight and flexible, allows easy integration with other frameworks and libraries.
Does not require an application server, you can use embedded servers (e.g. Tomcat, Jetty).
Java EE (Jakarta EE):
is a platform standard for developing enterprise applications in Java, providing a set of specifications and APIs.
Based on application servers such as WildFly, GlassFish, WebLogic, and JBoss, which provide all the necessary components.
Standardized and managed by a consortium (formerly Oracle, now the Eclipse Foundation).
More often used in large enterprise applications with high reliability and security requirements.
Containers and dependency injection:
Spring:
Based on an IoC container that manages the life cycle of objects and their dependencies.
Supports several configuration methods: XML, annotations, JavaConfig.
Java EE (Jakarta EE):
Uses containers to manage the life cycle of components (EJB, servlets).
Dependency injection is performed using CDI (Contexts and Dependency Injection).
Components and modules:
Spring:
Includes many projects and modules, such as Spring MVC, Spring Data, Spring Security, Spring Boot, Spring Cloud, and others.
Spring Boot makes it much easier to build and configure applications by providing ready-made templates and auto-configuration.
Java EE (Jakarta EE):
Consists of many specifications such as Servlet, JSP, JSF, JPA, EJB, JAX-RS, JMS, and more.
All specifications are interconnected and standardized, ensuring compatibility between different implementations.
Community and Support:
Spring:
Active community and strong support from Pivotal (now part of VMware).
Rapid adoption of new technologies and updates.
Java EE (Jakarta EE):
Community-driven under the auspices of the Eclipse Foundation (formerly Oracle).
Updates and new versions are released less frequently, but the specifications are thoroughly tested and standardized.
Tools and Integration:
Spring:
Easily integrates with various tools and libraries such as Hibernate, MyBatis, Apache Kafka, and more.
Spring Boot provides built-in servers, making development and testing easier.
Java EE (Jakarta EE):
Requires full-featured application servers, which can be more complex and difficult to initially set up.
Integrates well with other Java specifications and third-party libraries.
➤ What is aspect-oriented programming (AOP)?
Aspect-oriented programming (AOP) is a programming paradigm that allows the separation of cross-cutting aspects of an application from its core business logic. The primary goal of AOP is to improve the modularity of an application by providing a way to isolate and reuse code that crosses multiple modules or classes, such as logging, transaction management, security, and others.
The main concepts of AOP are:
Aspect:
A module containing cross-cutting functionality. An aspect can contain one or more pointcuts and advice.
Advice:
An action performed by an aspect. Advice types include:
Before: Performed before a method is executed.
After: Performed after a method is executed.
AfterReturning: Performed after a method has successfully completed.
AfterThrowing: Performed after a method has thrown an exception.
Around: Performed before and after a method is executed.
Pointcut:
A condition that determines which methods and objects an advice will be applied to. A pointcut is used to determine where the advice should be executed.
Join Point:
A specific location in program execution, such as a method call or exception handling, where the advice can be applied.
Weaving:
The process of associating aspects with target objects to create proxy objects. In Spring AOP, weaving occurs at runtime.
➤ How does Spring process a request and return a response?
Processing a request and emitting a response in Spring MVC (Model-View-Controller) occurs through a chain of interactions between several components. The main stages of request processing in Spring include:
Receiving a request:
A web server (e.g. Tomcat) receives an HTTP request.
Passing a request:
The web server passes the request to Spring’s DispatcherServlet.
Processing the request by DispatcherServlet:
Processing through filters:
The request can be passed through filters for preliminary processing.
Determining a controller:
DispatcherServlet uses HandlerMapping to determine the appropriate controller to handle the request.
Invoking a controller method:
The selected controller method processes the request and returns the result.
Processing through interceptors:
The request and response can be passed through interceptors for additional processing.
Generate a response:
DispatcherServlet passes the result to ViewResolver to render the view (if needed) or returns the data directly (e.g. JSON).
Send a response to the client:
The web server sends the constructed HTTP response to the client.
Request processing flow in detail:
Receive a request:
When a client (e.g. browser or Postman) sends an HTTP request to the server, the request reaches the web server (e.g. Apache Tomcat).
Pass the request to DispatcherServlet:
The web server passes the request to Spring’s DispatcherServlet. DispatcherServlet is the Front Controller for all incoming requests in Spring MVC.
Process the request to DispatcherServlet:
Process via filters:
Filters can be used to perform pre-processing of requests such as logging, authentication, authorization, etc.
Defining a controller:
DispatcherServlet uses one or more HandlerMapping to determine which controller and which controller method should handle the request. HandlerMapping determines controllers based on URL patterns, annotations, and other criteria.
Invoking a controller method:
After defining a controller, DispatcherServlet calls the appropriate controller method. The controller method processes the request, performs the necessary business logic, and generates the result.
Processing via interceptors:
Interceptors (HandlerInterceptor) can be used to perform additional processing before and after invoking a controller method. Interceptors can modify requests and responses, as well as perform additional checks and logging.
Forming a response:
Returning data directly:
If a controller method is annotated with @ResponseBody or a controller is annotated with @RestController, the returned data (for example, an object or list of objects) is automatically converted to JSON or XML using HttpMessageConverter.
Rendering a view:
If a controller method returns a view name (e.g. an HTML template), the DispatcherServlet uses the ViewResolver to render the view. The ViewResolver determines which template to use to generate the HTML response.
Sending a response to the client:
After generating the response (whether it’s JSON, XML, or HTML), the web server sends an HTTP response back to the client.
➤ What is Project Reactor
High-performance asynchronous reactive programming library for the JVM, developed by Pivotal (now VMware) as part of the Spring ecosystem. Reactor is the primary implementation of the Reactive Streams specification, and it provides powerful tools for creating asynchronous, event-driven programs and systems.
Key features of Project Reactor:
Reactive types:
Mono:
Represents an asynchronous stream that can contain 0 or 1 elements. Similar to Future or CompletableFuture, but with additional reactive operators.
Flux:
Represents an asynchronous stream that can contain 0, 1, or more elements (a data stream).
Rich set of operators:
Supports a variety of operators for working with data, such as filtering, transformation, joining, aggregation, and more.
Asynchrony and non-blocking I/O:
Reactor supports asynchronous execution and non-blocking I/O, making it ideal for building high-performance and scalable applications.
Reactive Streams specification support:
Reactor complies with the Reactive Streams specification, ensuring compatibility with other libraries and frameworks that support Reactive Streams.
Integration with the Spring ecosystem:
Reactor is tightly integrated with the Spring Framework and Spring Boot, providing reactive capabilities for building web applications, interacting with databases, and more.
import reactor.core.publisher.Mono fun main() { val mono: Mono<String> = Mono.just("Hello, Reactor!") mono.subscribe( { value -> println(value) }, // onNext { error -> println("Error: $error") }, // onError { println("Completed") } // onComplete ) }
import reactor.core.publisher.Flux fun main() { val flux: Flux<String> = Flux.just("Hello", "Reactor", "World") flux.subscribe( { value -> println(value) }, // onNext { error -> println("Error: $error") }, // onError { println("Completed") } // onComplete ) }
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Flux import reactor.core.publisher.Mono @RestController class GreetingController { @GetMapping("/hello") fun hello(): Mono<String> { return Mono.just("Hello, Spring WebFlux!") } @GetMapping("/greetings") fun greetings(): Flux<String> { return Flux.just("Hello", "Hi", "Hey", "Hola") } }
➤ What is Mono and Flux
Mono and Flux are key types in the Reactor project, which is the foundation for reactive programming in Spring WebFlux. These types represent reactive streams of data and are used to handle asynchronous operations.
Mono:
Represents a stream that can contain zero or one values. It is used to represent an asynchronous operation that returns a single value or completes without returning a value (like a Mono).
The main methods of Mono are:
just(value: T):
Creates a Mono that returns the specified value.
empty():
Creates an empty Mono.
error(throwable: Throwable):
Creates a Mono that will fail with an error.
fromCallable(callable: Callable):
Creates a Mono from a function that returns a value.
map(transform: Function):
Converts a value to a Mono using a function.
flatMap(transform: Function):
Converts a value to a Mono returned by a function and subscribes to it.
then():
Ignores the result and returns Mono
import org.springframework.web.bind.annotation.* import reactor.core.publisher.Mono @RestController @RequestMapping("/users") class UserController(private val userService: UserService) { @GetMapping("/{id}") fun getUserById(@PathVariable id: String): Mono<User> { return userService.getUserById(id) } @PostMapping fun createUser(@RequestBody user: User): Mono<User> { return userService.createUser(user) } @DeleteMapping("/{id}") fun deleteUser(@PathVariable id: String): Mono<Void> { return userService.deleteUser(id) } }
Flux:
Represents a stream that can contain zero or more values. It is used to represent an asynchronous data flow that can generate multiple values, such as collections or continuous streams of data.
The main methods of Flux are:
just(values: T…):
Creates a Flux that returns the specified values.
empty():
Creates an empty Flux.
error(throwable: Throwable):
Creates a Flux that will fail with an error.
fromIterable(iterable: Iterable):
Creates a Flux from a collection.
range(start: Int, count: Int):
Creates a Flux that generates a sequence of numbers.
map(transform: Function):
Transforms each value into a Flux using a function.
flatMap(transform: Function):
Transforms each value into a Flux returned by a function and subscribes to it.
filter(predicate: Predicate):
Filters values in Flux using a predicate.
import org.springframework.web.bind.annotation.* import reactor.core.publisher.Flux @RestController @RequestMapping("/users") class UserController(private val userService: UserService) { @GetMapping fun getAllUsers(): Flux<User> { return userService.getAllUsers() } @GetMapping("/role/{role}") fun getUsersByRole(@PathVariable role: String): Flux<User> { return userService.getUsersByRole(role) } @GetMapping("/usernames") fun getUsernames(): Flux<String> { return userService.getUsernames() } }
Mono vs Flux:
Mono:
Used when zero or one value is expected.
Example: Getting a user by ID (one or none).
Flux:
Used when zero or more values are expected.
Example: Getting a list of all users (can be zero, one or many users).
Example using map, flatMap and filter operations:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.time.Duration @RestController class HomeController { @GetMapping("/") fun index(): Mono<String> { return Mono.just("Welcome to the home page!") .map { it.toUpperCase() } // Convert the string to uppercase .flatMap { appendDate(it) } // Add the current date .filter { it.length > 20 } // Filter lines longer than 20 characters .switchIfEmpty(Mono.just("The resulting string is too short!")) // If the string is shorter than 20 characters, return an alternative message } private fun appendDate(str: String): Mono<String> { return Mono.just("$str - ${java.time.LocalDate.now()}") } }
Example using doOnNext and delayElement:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.time.Duration @RestController class HomeController { @GetMapping("/") fun index(): Mono<String> { return Mono.just("Welcome to the home page!") .doOnNext { println("Original String: $it") } // Log the original string .map { it.reversed() } // Reverse the string .delayElement(Duration.ofSeconds(2)) // 2 seconds delay .doOnNext { println("Reversed String: $it") } // Log the inverted string } }
Example using zipWith to combine with another Mono:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono @RestController class HomeController { @GetMapping("/") fun index(): Mono<String> { val additionalInfo = Mono.just("This is additional info") return Mono.just("Welcome to the home page!") .zipWith(additionalInfo) { original, additional -> "$original - $additional" } } }
Example using flatMapMany to convert to Flux and then merge:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Flux import reactor.core.publisher.Mono @RestController class HomeController { @GetMapping("/") fun index(): Mono<String> { return Mono.just("Welcome to the home page!") .flatMapMany { Flux.fromArray(it.split(" ").toTypedArray()) } // Convert the string to a Flux of words .map { it.reversed() } // Reverse each word .collectList() // Collect back into a list .map { it.joinToString(" ") } // Combine the words back into a string } }
➤ What is a Servlet?
Servlets are server-side components written in Java that are used to process requests and generate dynamic content for web applications. They run on the server and are part of Java EE (now Jakarta EE) technology. Servlets allow you to create web applications that can interact with clients over HTTP.
Main concepts of servlets:
Servlet container:
The runtime environment in which servlets run. The container manages the servlet’s life cycle, its initialization, request processing, and destruction.
HttpServlet:
The class from which all servlets inherit to handle HTTP requests.
Servlet API:
A set of classes and interfaces that provide functionality for creating servlets.
Servlet life cycle:
Initialization (init):
The init method is called by the container when the servlet is created. This method initializes the servlet and is executed once per servlet lifecycle.
Request handling (service):
The service method is called every time the servlet receives a request from a client. This method delegates requests to doGet, doPost, doPut, doDelete, etc., depending on the type of HTTP request.
Destroy:
The destroy method is called by the container before destroying the servlet. This method is executed once and is used to free up resources.
A simple servlet example:
import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import java.io.IOException @WebServlet("/hello") class HelloServlet : HttpServlet() { @Throws(ServletException::class, IOException::class) override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { response.contentType = "text/html" response.writer.use { out -> out.println("<html><body>") out.println("<h1>Hello, World!</h1>") out.println("</body></html>") } } }
Servlet configuration in web.xml:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.example.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>
Deploying the application:
Place your servlet and web.xml configuration file in the appropriate directories of your web application (e.g. WEB-INF/classes and WEB-INF respectively). Deploy your application to an application server such as Tomcat, WildFly or GlassFish.
Handling different types of HTTP requests:
Servlets can handle different types of HTTP requests such as GET, POST, PUT, DELETE, etc. To do this, you need to override the appropriate methods in the servlet class.
import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import java.io.IOException @WebServlet("/example") class ExampleServlet : HttpServlet() { @Throws(ServletException::class, IOException::class) override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { } @Throws(ServletException::class, IOException::class) override fun doPost(request: HttpServletRequest, response: HttpServletResponse) { } @Throws(ServletException::class, IOException::class) override fun doPut(request: HttpServletRequest, response: HttpServletResponse) { } @Throws(ServletException::class, IOException::class) override fun doDelete(request: HttpServletRequest, response: HttpServletResponse) { } }
Using Request Parameters and Sessions:
Servlets can extract request parameters and work with sessions to persist data between requests.
Extracting Request Parameters
import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import java.io.IOException @WebServlet("/param") class ParamServlet : HttpServlet() { @Throws(ServletException::class, IOException::class) override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { val param = request.getParameter("paramName") response.contentType = "text/html" response.writer.use { out -> out.println("<html><body>") out.println("<h1>Parameter: $param</h1>") out.println("</body></html>") } } }
Working with sessions:
import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpSession import java.io.IOException @WebServlet("/session") class SessionServlet : HttpServlet() { @Throws(ServletException::class, IOException::class) override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { val session: HttpSession = request.session session.setAttribute("username", "JohnDoe") response.contentType = "text/html" response.writer.use { out -> out.println("<html><body>") out.println("<h1>Session created for user: ${session.getAttribute("username")}</h1>") out.println("</body></html>") } } }
➤ What is Public Key Infrastructure (PKI)
Public Key Infrastructure (PKI) is a system that enables the management, creation, distribution, use, storage, and revocation of digital certificates and public keys. PKI is the foundation for secure data exchange on the Internet, ensuring the authenticity, integrity, and confidentiality of information.
The main components of a PKI are:
Certificate Authority (CA):
is a trusted third party that issues and manages digital certificates. The CA verifies the identity of the entity to which the certificate is issued.
Registration Authority (RA):
is responsible for verifying the identity of the entity before the CA issues the certificate. The RA acts as an intermediary between the user and the CA.
Digital Certificate:
an electronic document that associates a public key with the identity of its owner (entity). The certificate contains information about the owner, the public key, the validity period, and the CA’s digital signature.
Public and Private Keys:
The public key is used to encrypt data and verify digital signatures. It is distributed publicly.
The private key is used to decrypt data and create digital signatures. It is kept secret.
Certificate Store:
A database that stores digital certificates, private keys, and other elements of the PKI.
Certificate Revocation List (CRL):
A list of certificates that have been revoked before their expiration date. The CA regularly updates and distributes the CRL.
Protocols and Standards:
PKI uses various standards and protocols, such as X.509 for digital certificates, SSL/TLS for secure connections, and others.
How PKI Works:
Certificate Request:
A user (or device) generates a key pair (public and private keys).
The user sends a Certificate Signing Request (CSR) to the CA via the RA. The CSR contains the user information and public key.
Certificate Verification and Issuance:
The RA verifies the user’s identity.
The CA creates a digital certificate, signs it with its private key, and issues it to the user.
Certificate Usage:
The user uses their digital certificate and its associated private key to securely exchange data and digitally sign it.
Certificate Authentication:
The recipient of the data uses the CA’s public key to verify the digital signature of the user’s certificate, confirming its authenticity.
Certificate Revocation:
If the private key is compromised or the user’s status changes, the certificate can be revoked. The CA adds the revoked certificate to the CRL.
PKI Usage Example:
Let’s look at a simple example of using PKI to secure a website with an SSL/TLS certificate.
Requesting and Issuing an SSL Certificate:
The website owner generates a key pair (public and private keys).
The website owner sends the CSR to the CA, providing the domain information and the public key.
The CA verifies the ownership of the domain and issues an SSL certificate, signing it with its private key.
Configuring the Website:
The website owner installs the SSL certificate and its associated private key on the web server.
The web server is configured to use SSL/TLS to establish secure connections.
Secure Connection:
When a client (such as a browser) connects to the website, the server sends its SSL certificate to the client.
The client verifies the authenticity of the certificate using the CA’s public key.
If the certificate is valid, a secure connection is established and the data is encrypted using the server’s public key.
➤ What is CSRF (Cross-Site Request Forgery)
CSRF (Cross-Site Request Forgery) is a type of web application attack in which an attacker tricks a user into performing an unwanted action on a site where they are authenticated. The attack is carried out by sending fake requests on behalf of the user, using their authentication data (e.g. session cookies).
How CSRF works:
A user is authenticated to a website and has an active session.
The attacker creates a fake form or link on their site that sends requests to the target site on behalf of the user.
The user, while visiting the attacker’s site, accidentally performs an action on the target site (e.g. transferring money or changing a password).
How to prevent CSRF in Spring Security:
Spring Security provides built-in protection against CSRF attacks, which is enabled by default. The protection works by using CSRF tokens, which must be included in each state-changing request (e.g. POST, PUT, DELETE).
How CSRF protection works in Spring Security:
When a page with a form is loaded, Spring Security generates a unique CSRF token and adds it to a hidden field in the form.
When the form is submitted, the token is sent along with the request.
The server checks for the presence and validity of the token. If the token is missing or invalid, the request is rejected.
Example of configuring CSRF protection in Spring Security:
Enabling CSRF protection (usually enabled by default)
CSRF protection is enabled by default in Spring Security. To ensure this, you can explicitly specify the configuration in the SecurityConfig class.
import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter @Configuration @EnableWebSecurity class SecurityConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http.authorizeRequests() .antMatchers("/public/**", "/login", "/register").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/home", true) .permitAll() .and() .csrf() // Enable CSRF protection (enabled by default) } }
Adding a CSRF token to forms:
Spring Security automatically adds a CSRF token to the form if Thymeleaf is used. For other template engines or manual processing, you must explicitly include the token in the form.
Example of a form using Thymeleaf
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Login</title> </head> <body> <h1>Login</h1> <form th:action="@{/login}" method="post"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <div> <label for="username">Username:</label> <input type="text" id="username" name="username" required /> </div> <div> <label for="password">Password:</label> <input type="password" id="password" name="password" required /> </div> <div> <button type="submit">Login</button> </div> </form> </body> </html>
In this example, the line adds the CSRF token to the form.
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
Handling AJAX requests with CSRF:
For AJAX requests, you must include the CSRF token in the request header.
Example of an AJAX request with a CSRF token
function sendPostRequest() { var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $.ajax({ url: '/your-endpoint', type: 'POST', contentType: 'application/json', data: JSON.stringify({ key: 'value' }), beforeSend: function(xhr) { xhr.setRequestHeader(header, token); }, success: function(response) { console.log('Success:', response); }, error: function(error) { console.log('Error:', error); } }); }
Adding meta tags for CSRF token to HTML:
<meta name="_csrf" content="${_csrf.token}"/> <meta name="_csrf_header" content="${_csrf.headerName}"/>
➤ What is Prometheus format
The Prometheus format is a standard format for collecting, storing, and presenting metrics that is used by the Prometheus monitoring system. This format includes various metric types, labels for additional information, and is used for easy analysis and aggregation of data.
The main elements of the Prometheus format are:
Metric types:
Prometheus supports several metric types:
Counter:
A monotonically increasing value that is used to count something (e.g. number of requests).
Gauge:
A value that can increase and decrease (e.g. current memory usage).
Histogram:
Sizes values into fixed intervals (buckets), and is used to measure the distribution of values (e.g. response time of requests).
Summary:
Similar to a histogram, but also stores quantile estimates (e.g. 95th percentile of response time of requests).
Labels:
Labels are used to add additional information to metrics. They help filter and aggregate metrics across different dimensions.
Data format:
Prometheus formatted metrics are represented as text strings with a specific structure.
# HELP http_requests_total The total number of HTTP requests. # TYPE http_requests_total counter http_requests_total{method="get",handler="/api"} 1027 http_requests_total{method="post",handler="/api"} 230 # HELP cpu_usage The current CPU usage. # TYPE cpu_usage gauge cpu_usage{core="0"} 0.75 cpu_usage{core="1"} 0.60 # HELP request_duration_seconds A histogram of the request duration. # TYPE request_duration_seconds histogram request_duration_seconds_bucket{le="0.1"} 24054 request_duration_seconds_bucket{le="0.2"} 33444 request_duration_seconds_bucket{le="0.5"} 100392 request_duration_seconds_bucket{le="1.0"} 129389 request_duration_seconds_bucket{le="+Inf"} 144320 request_duration_seconds_sum 53423 request_duration_seconds_count 144320 # HELP response_size_bytes The size of HTTP responses. # TYPE response_size_bytes summary response_size_bytes{quantile="0.5"} 512 response_size_bytes{quantile="0.9"} 1024 response_size_bytes{quantile="0.99"} 2048 response_size_bytes_sum 10554321 response_size_bytes_count 24530
Comments:
Comments start with # and provide additional information about the metrics, such as a description (HELP) and the metric type (TYPE).
Metrics:
Each metric row consists of the metric name, labels (if any), and value.
Metric types:
http_requests_total:
A counter tracking the number of HTTP requests.
cpu_usage:
A gauge tracking the current CPU usage.
request_duration_seconds:
A histogram tracking the duration of requests.
response_size_bytes:
A summary tracking the sizes of HTTP responses.
How to use the Prometheus format:
dependencies { implementation 'io.micrometer:micrometer-registry-prometheus' }
import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.context.annotation.Bean import io.micrometer.core.instrument.MeterRegistry import io.micrometer.prometheus.PrometheusMeterRegistry import io.micrometer.prometheus.PrometheusConfig @SpringBootApplication class Application fun main(args: Array<String>) { SpringApplication.run(Application::class.java, *args) } @Bean fun prometheusMeterRegistry(): PrometheusMeterRegistry { return PrometheusMeterRegistry(PrometheusConfig.DEFAULT) } @Bean fun metricsCommonTags(): MeterRegistryCustomizer<MeterRegistry> { return MeterRegistryCustomizer { registry -> registry.config().commonTags("application", "my-app") } }
Method for exposing metrics:
import io.micrometer.prometheus.PrometheusMeterRegistry import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController class MetricsController( private val prometheusMeterRegistry: PrometheusMeterRegistry ) { @GetMapping("/actuator/prometheus") fun scrape(): String { return prometheusMeterRegistry.scrape() } }
➤ What is Structured Concurrency?
Structured concurrency is a programming concept aimed at simplifying the work with parallelism and concurrency. The basic idea is that all parallel tasks should be structured in such a way that their life cycles and task hierarchies are clearly defined and managed. This allows for easier task management, improved code readability and reliability, and easier debugging and error handling.
The main principles of structured concurrency are:
Task hierarchy:
Parallel tasks are organized into a clear hierarchy, where each task has a parent and child tasks.
The life cycle of a child task is controlled by the parent task.
Automatic task life cycle management:
A parent task is responsible for creating and terminating all its child tasks. Completion of a parent task automatically leads to the completion of all its child tasks.
Explicit task scope management:
The scope of parallel tasks is limited to code blocks, which simplifies the management of tasks and their resources.
➤ What is the Happens-Before Principle (event precedence)
The Happens-Before principle is a key concept in multithreading and concurrency theory that describes the ordering of operations and events in a multithreaded environment. This principle is used to determine the correctness of program execution and to ensure synchronization between threads.
The main ideas of the Happens-Before principle are:
Sequence of execution in a single thread:
If one operation precedes another in the same thread, then the first operation will necessarily occur before the second. This ensures the program logic and ordering of operations within a single thread.
Synchronization using locks:
If operation A unlocks a monitor (lock), and operation B acquires the same monitor, then operation A occurs before operation B. This ensures that changes made by a thread before unlocking are visible to another thread after acquiring the lock.
Synchronization with events (e.g. Thread.join()):
If operation A calls Thread.join() on thread B, then all operations in thread B until it completes happen before Thread.join() returns in thread A.
Synchronization with volatile variables:
A write to a volatile variable happens before any subsequent reads of that variable by other threads. This ensures that changes are visible.
Transitivity:
If operation A happens before operation B, and operation B happens before operation C, then operation A happens before operation C.
➤ What is the difference between WAR (Web Application Archive) and JAR (Java Archive)
WAR (Web Application Archive) and JAR (Java Archive) are two different archive formats used to package and distribute applications on the Java platform.
JAR files:
used to package Java programs and libraries
WAR files:
intended for packaging and deploying web applications on an application server.
➤ What is the life cycle of beans in Spring
The life cycle of a bean in the Spring Framework includes several stages, from creation to destruction. Understanding the bean life cycle is important for proper resource management and configuration of beans in Spring-based applications.
The main stages of the bean life cycle in Spring are:
Instance creation:
Creation of a bean instance begins with a call to the bean class constructor. This process is typically performed by the Spring container using reflection.
Dependency injection:
After the instance is created, the Spring container injects dependencies (using setters, constructors, or fields annotated with @Autowired).
Initialization setup (Setters and @PostConstruct):
After dependency injection, Spring calls the setup and initialization methods.
Methods annotated with @PostConstruct are called after dependency injection, but before the bean is used.
Initialization:
If the bean implements the InitializingBean interface, Spring calls the afterPropertiesSet() method.
You can also specify the initialization method using the @Bean(initMethod=”init”) annotation in the configuration class.
Using a bean:
After all initialization methods have been executed, the bean is ready to be used in the application.
Deinitialization (DisposableBean and @PreDestroy):
When the Spring container is closed (for example, when the application is terminated), the beans are destroyed.
If the bean implements the DisposableBean interface, Spring calls the destroy() method.
Methods annotated with @PreDestroy are also called before the bean is destroyed.
You can also specify the deinitialization method using the @Bean(destroyMethod=”cleanup”) annotation in the configuration class.
Example of a bean lifecycle:
import javax.annotation.PostConstruct import javax.annotation.PreDestroy import org.springframework.beans.factory.DisposableBean import org.springframework.beans.factory.InitializingBean class MyBean : InitializingBean, DisposableBean { init { println("1. Constructor: Create a bean instance") } @PostConstruct fun postConstruct() { println("3. @PostConstruct: Method postConstruct()") } override fun afterPropertiesSet() { println("4. InitializingBean: Method afterPropertiesSet()") } fun customInit() { println("5. @Bean(initMethod): Method customInit()") } @PreDestroy fun preDestroy() { println("7. @PreDestroy: Method preDestroy()") } override fun destroy() { println("6. DisposableBean: Method destroy()") } fun customDestroy() { println("8. @Bean(destroyMethod): Method customDestroy()") } }
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class AppConfig { @Bean(initMethod = "customInit", destroyMethod = "customDestroy") fun myBean(): MyBean { return MyBean() } }
import org.springframework.context.annotation.AnnotationConfigApplicationContext fun main() { val context = AnnotationConfigApplicationContext(AppConfig::class.java) context.getBean(MyBean::class.java) context.close() }
Output when executing the program:
1. Constructor: Create a bean instance 2. Dependency injection (if any) 3. @PostConstruct: Method postConstruct() 4. InitializingBean: Method afterPropertiesSet() 5. @Bean(initMethod): Method customInit() 6. Using bin 7. @PreDestroy: Method preDestroy() 8. DisposableBean: Method destroy() 9. @Bean(destroyMethod): Method customDestroy()
Constructor:
Spring creates a bean instance by calling its constructor.
Dependency Injection:
Spring injects all dependencies into the bean (if any).
@PostConstruct:
The method annotated with @PostConstruct is called.
InitializingBean:
If the bean implements the InitializingBean interface, the afterPropertiesSet() method is called.
@Bean(initMethod):
If an initialization method is specified in the configuration, it is called.
Using the bean: The bean is ready for use.
@PreDestroy:
Before the bean is destroyed, the method annotated with @PreDestroy is called.
DisposableBean:
If the bean implements the DisposableBean interface, the destroy() method is called.
@Bean(destroyMethod):
If a deinitialization method is specified in the configuration, it is called.
➤What is Git Registry, GitHub Packages, GitHub Actions, Changelog, Release Notes
Git Registry:
is a term commonly used to describe a system for managing versions and artifacts of software built on top of Git. This can include hosting and managing packages such as Docker images, libraries for various programming languages, and other artifacts.
GitHub Packages:
is GitHub’s built-in package hosting and management service. It allows developers to publish, manage, and use packages in their projects using the same GitHub account and repositories.
GitHub Actions:
is GitHub’s built-in CI/CD automation platform. It allows you to build, test, and deploy code directly from a GitHub repository using workflows written in a YAML file.
Changelog:
is a document that contains a list of changes made to a project with each version. A changelog helps track changes, bug fixes, new features, and other important events in a project’s history.
Release Notes:
A document that provides information about a specific version of software, including new features, enhancements, bug fixes, and known issues. Release Notes are often published along with the release of a new version and help users understand what has changed.
➤ What are the main data exchange protocols in Spring
SOAP (Simple Object Access Protocol):
is a protocol for exchanging structured information messages in distributed computing environments. SOAP is a standard for web services and allows applications to interact over a network regardless of their platform and the technologies used to implement them.
A SOAP message consists of the following main parts:
Envelope: The main element that defines the beginning and end of the message. It includes two subelements:
Header (optional): Contains metainformation about the message, such as security, transaction, and routing information.
Body: Contains the main content of the message, including method calls and responses.
Fault (optional): An element used to convey error information.
An example of a SOAP message:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://www.example.org/webservice"> <soapenv:Header/> <soapenv:Body> <web:GetUserDetails> <web:UserId>12345</web:UserId> </web:GetUserDetails> </soapenv:Body> </soapenv:Envelope>
HTTP (Hypertext Transfer Protocol) and HTTPS (HTTP Secure):
The primary protocols used to transfer data between clients and servers in web applications. Spring MVC and Spring WebFlux use HTTP/HTTPS to handle web requests and responses.
REST (Representational State Transfer):
An architectural style that uses the HTTP protocol to create web services that communicate using standard HTTP methods (GET, POST, PUT, DELETE, etc.). In Spring Boot, RESTful web services are implemented using annotations such as @RestController, @GetMapping, @PostMapping, etc.
WebSocket:
A protocol for two-way communication between a client and a server over a single, long-lived connection. WebSockets are often used for real-time applications such as chat rooms and online games.
JMS (Java Message Service):
A standard API for sending messages and asynchronous communication between distributed components. Spring provides integration with JMS via Spring JMS.
RSocket:
A protocol for asynchronous data transfer and communication between services. Spring supports RSocket via Spring Messaging and Spring Boot.
AMQP (Advanced Message Queuing Protocol):
A protocol for exchanging messages between system components. Spring supports AMQP via the Spring AMQP project, which includes integration with RabbitMQ.
➤ What is Protobuf
Protobuf (Protocol Buffers) is a data serialization mechanism developed by Google. It is used to efficiently exchange data between systems and store data. Protobuf allows you to define data structures and automatically generate code for serializing and deserializing these structures in various programming languages.
The main steps of working with Protobuf:
Defining the data schema.
Compiling the schema into code for the selected programming language.
Using the generated code to serialize and deserialize data.
An example of using Protobuf in Spring Boot:
Defining the data schema:
Create a Protobuf schema file, for example user.proto:
syntax = "proto3"; package com.example.demo; message User { int32 id = 1; string name = 2; string email = 3; }
plugins { id("com.google.protobuf") version "0.8.17" id("org.springframework.boot") version "2.5.4" id("io.spring.dependency-management") version "1.0.11.RELEASE" kotlin("jvm") version "1.5.21" kotlin("plugin.spring") version "1.5.21" } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.google.protobuf:protobuf-java:3.17.3") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.17.3" } generatedFilesBaseDir = "$projectDir/src/generated" } sourceSets { main { java { srcDirs("src/generated/main/java") } } }
import com.example.demo.UserProtos.User import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @RestController @RequestMapping("/api") class UserController { @GetMapping("/user") fun getUser(): ByteArray { val user = User.newBuilder() .setId(1) .setName("John Doe") .setEmail("[email protected]") .build() val outputStream = ByteArrayOutputStream() user.writeTo(outputStream) return outputStream.toByteArray() } @GetMapping("/user-from-bytes") fun getUserFromBytes(): User { val userBytes = getUser() val inputStream = ByteArrayInputStream(userBytes) return User.parseFrom(inputStream) } }
# Getting serialized user curl -X GET http://localhost:8080/api/user --output user.protobuf # Getting user from bytes curl -X GET http://localhost:8080/api/user-from-bytes
➤ What is Javadoc
Javadoc is a tool for automatically generating documentation for Java code. Javadoc takes comments written in a special format and generates HTML documentation from them. It is one of the standards for documenting code in Java and is widely used in professional development to create understandable and maintainable documentation.
The main elements of Javadoc:
Javadoc uses special comments that start with /** and end with */. Inside these comments, tags are used to describe different parts of the code.
The main Javadoc tags:
@param:
Description of the method parameter.
@return:
Description of the return value of the method.
@throws or @exception:
Description of the exception that can be thrown by the method.
@see:
Reference to another part of the code or documentation.
@since:
The version in which this element was added.
@deprecated:
Indicates that the element is obsolete and is not recommended for use.
@author:
The author of the code.
@version:
The version of the code.
/** * The class represents a model of a user in the system. * <p> * This class is used to store information about the user, * including their ID, name, and email. * </p> * * @author John Doe * @version 1.0 * @since 2023-07-05 */ public class User { private Long id; private String name; private String email; /** * The constructor creates a new user with the given name and email. * * @param name Username. * @param email User's email. */ public User(String name, String email) { this.name = name; this.email = email; } /** * Returns the user ID. * * @return User ID. */ public Long getId() { return id; } /** * Sets the user ID. * * @param id User ID. */ public void setId(Long id) { this.id = id; } /** * Returns the username. * * @return Username. */ public String getName() { return name; } /** * Sets the user name. * * @param name Username. */ public void setName(String name) { this.name = name; } /** * Returns the user's email. * * @return User's email. */ public String getEmail() { return email; } /** * Sets the user's email. * * @param email User's email. */ public void setEmail(String email) { this.email = email; } }
To generate documentation from Javadoc comments, the javadoc tool, which is included in the JDK, is used.
javadoc -d doc -sourcepath src com.example.demo
d doc:
Specifies the directory for the output HTML files.
sourcepath src:
Specifies the path to the source files.
com.example.demo:
Specifies the package for which documentation will be generated.
Javadoc integration with Gradle:
plugins { id 'java' id 'maven-publish' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web:2.5.4' implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.5.4' implementation 'com.h2database:h2:1.4.200' testImplementation 'org.springframework.boot:spring-boot-starter-test:2.5.4' } tasks.withType(Javadoc) { options { encoding = 'UTF-8' charSet = 'UTF-8' links 'https://docs.oracle.com/en/java/javase/11/docs/api/' } } task javadocJar(type: Jar) { archiveClassifier = 'javadoc' from javadoc } publishing { publications { mavenJava(MavenPublication) { from components.java artifact javadocJar } } repositories { maven { url = uri("$buildDir/repo") } } }
tasks.withType(Javadoc):
Configure Javadoc generation parameters, including encoding and links to external documentation.
task javadocJar:
Create a task to create a JAR file with Javadoc.
publishing:
Configure publishing of artifacts, including JAR with Javadoc, to a local repository.
➤ What are the main differences between Maven and Gradle
Syntax and Declarativity:
Maven uses XML (POM file) to describe the project and its configuration. This makes it declarative and structured, but sometimes quite cumbersome and hard to read.
Gradle uses Groovy or Kotlin DSL, which makes it more flexible and concise. Groovy and Kotlin syntax allows you to write configurations that are more readable and support programming.
Performance:
Gradle is usually faster due to its incremental build and caching. Gradle only runs tasks that have changed and uses caching to avoid repetitive operations.
Maven is more linear and sequential in executing tasks, which can lead to longer builds compared to Gradle.
Flexibility and Extensibility:
Gradle is more flexible due to the ability to use a programming language (Groovy or Kotlin) to describe configurations. This allows you to easily extend and customize build processes.
Maven is less flexible due to its declarative nature. Customizations and extensions usually require writing and including additional plugins.
Multi-project build support:
Gradle natively supports multi-project builds, making it more suitable for large projects with multiple modules.
Maven also supports multi-project builds, but the setup can be more complex and less intuitive than Gradle.
Configuration files:
Maven uses one main configuration file, pom.xml.
Gradle uses build.gradle for each project or module, and can also include settings.gradle for multi-project builds.
Plugins and dependencies:
Gradle has a more modern plugin and dependency management system. Plugins can be included via the plugins block, making it simpler and more intuitive.
Maven uses plugins that must be explicitly declared in pom.xml.
Script support:
Gradle allows you to write logical conditions and loops directly in the configuration file, making it more powerful for complex build scenarios.
Maven is more declarative, making it less convenient to write complex logic.
IDE Integration:
Gradle is supported by most modern IDEs such as IntelliJ IDEA, Eclipse, and Android Studio, making it more convenient for Android developers.
Maven is also supported by all popular IDEs, but Gradle is more widely used in the Android development environment.
➤ What does pom.xml consist of?
Model Version:
POM model version
<modelVersion>4.0.0</modelVersion>
Group ID:
Unique identifier of the organization or group that creates the project
<groupId>com.example</groupId>
Artifact ID:
Unique identifier of the project itself.
<artifactId>my-app</artifactId>
Version:
Project version.
<version>1.0-SNAPSHOT</version>
Packaging:
The type of project packaging (e.g. jar, war, pom, etc.).
<packaging>jar</packaging>
Name:
Human-readable name of the project.
<name>My App</name>
Description:
Project description.
<description>My Maven Project</description>
Dependencies:
A section describing the project’s dependencies on other libraries or modules.
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies>
Repositories:
A section that specifies the repositories from which Maven will download dependencies..
<repositories> <repository> <id>central</id> <url>https://repo.maven.apache.org/maven2</url> </repository> </repositories>
Build:
Section describing the instructions for building the project.
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
Properties:
A section that defines properties that can be used in a POM file..
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
➤ What does build.gradle consist of
Plugins:
Connecting plugins, for example, to compile Java code.
plugins { id 'java' }
Group and Version:
Setting the project ID and version.
group 'com.example' version '1.0-SNAPSHOT'
Source Compatibility:
Installing the Java language version.
sourceCompatibility = 1.8
Repositories:
Specifying repositories from which dependencies will be downloaded.
repositories { mavenCentral() }
Dependencies:
Defining project dependencies.
dependencies { testImplementation 'junit:junit:4.12' }
Test:
Setting up tasks for testing.
test { useJUnitPlatform() }
Tasks with Type:
Setting up compilation tasks.
tasks.withType(JavaCompile) { options.encoding = 'UTF-8' sourceCompatibility = '1.8' targetCompatibility = '1.8' }
Jar:
Setting up the task of packaging into a JAR file.
jar { manifest { attributes( 'Implementation-Title': 'My App', 'Implementation-Version': version ) } }
➤ What is Supplier
In Java, Supplier is a functional interface from the java.util.function package that represents a function that takes no arguments and returns a result. Supplier is used for lazy initialization, generating values on demand, and implementing various design patterns such as factories.
The main features of Supplier are:
Functional interface:
An interface with a single abstract method.
The get method:
An abstract method that takes no arguments and returns a value of type T.
Example of using Supplier:
Supplier definition:
import java.util.function.Supplier; public class SupplierExample { public static void main(String[] args) { Supplier<String> stringSupplier = () -> "Hello, World!"; System.out.println(stringSupplier.get()); } }
Using Supplier for lazy initialization:
import java.util.function.Supplier; public class LazyInitializationExample { private Supplier<ExpensiveObject> expensiveObjectSupplier = this::createExpensiveObject; private ExpensiveObject expensiveObject; public ExpensiveObject getExpensiveObject() { if (expensiveObject == null) { expensiveObject = expensiveObjectSupplier.get(); } return expensiveObject; } private ExpensiveObject createExpensiveObject() { System.out.println("Creating Expensive Object..."); return new ExpensiveObject(); } public static void main(String[] args) { LazyInitializationExample example = new LazyInitializationExample(); System.out.println("First call:"); example.getExpensiveObject(); // Object will be created here System.out.println("Second call:"); example.getExpensiveObject(); // Object already created, no creation message } static class ExpensiveObject { // Some expensive operations } }
Using Supplier in collections:
import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; public class CollectionExample { public static void main(String[] args) { Map<String, Supplier<Object>> registry = new HashMap<>(); registry.put("key1", () -> "Value for key1"); registry.put("key2", () -> 12345); registry.put("key3", () -> new Object()); Object value1 = registry.get("key1").get(); Object value2 = registry.get("key2").get(); Object value3 = registry.get("key3").get(); System.out.println(value1); // Output: Value for key1 System.out.println(value2); // Output: 12345 System.out.println(value3); // Output: java.lang.Object@<hashcode> } }
Usage in Spring:
In Spring, Supplier can also be useful for lazy initialization of beans or factory methods.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.function.Supplier; @Configuration public class AppConfig { @Bean public Supplier<MyBean> myBeanSupplier() { return MyBean::new; } @Bean public MyService myService(Supplier<MyBean> myBeanSupplier) { return new MyService(myBeanSupplier.get()); } } class MyBean { // Bean definition } class MyService { private final MyBean myBean; public MyService(MyBean myBean) { this.myBean = myBean; } // Service methods }
➤ What is the difference between record in Java and data class in Kotlin
Record in Java and data class in Kotlin serve similar purposes — to create simple classes for storing data with minimal boilerplate code. However, there are some differences between them due to the peculiarities of the languages and their frameworks.
The main differences between record in Java and data class in Kotlin:
Immutability:
Java record: Fields of record are final and immutable by default.
Kotlin data class: Fields are not final by default and can be modified. However, you can declare them as val (immutable) or var (mutable).
Inheritance support:
Java record: Record does not support inheritance from other classes (but can implement interfaces).
Kotlin data class: Data class can inherit from other classes and implement interfaces.
Method generation:
Java record: Automatically generates a constructor, equals, hashCode, toString methods, and field accessors.
Kotlin data class: Automatically generates a constructor, equals, hashCode, toString methods, as well as copy and componentN methods (for destructuring).
Constructors:
Java record: Allows you to define additional constructors and methods, but automatically generates a compact constructor.
Kotlin data class: Supports primary and secondary constructors.
Usage features:
Java record: Used to create simple immutable data objects. Fields must be declared in the record header parameters.
Kotlin data class: Extensive capabilities, including copy methods for cloning objects with changes to individual fields.
➤ What is Java JWT (Java JSON Web Token)
Java JWT (JSON Web Token) library called JJWT is designed to create and validate JWT (JSON Web Token) tokens. JJWT (Java JWT) is an open source library that simplifies working with JWT in Java applications. JWT is a compact, URL-safe way to represent tokens that can be used to transfer authenticated information between two parties.
Key Features of JJWT:
JWT Creation:
Easily create JWTs with various payload and signature types.
JWT Parsing and Validation:
Ability to parse and validate JWT tokens to verify their authenticity and extract the payload.
Support for various signature algorithms:
Support for various signature algorithms such as HMAC, RSA, and EC.
dependencies { implementation("io.jsonwebtoken:jjwt-api:0.11.2") runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") }
import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.security.Keys import java.security.Key import java.util.Date object JwtUtil { private val key: Key = Keys.secretKeyFor(SignatureAlgorithm.HS256) fun createToken(subject: String): String { return Jwts.builder() .setSubject(subject) .setIssuedAt(Date()) .setExpiration(Date(System.currentTimeMillis() + 3600000)) // 1 hour .signWith(key) .compact() } fun parseToken(token: String): String? { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .body .subject } }
Create a key:
Keys.secretKeyFor(SignatureAlgorithm.HS256):
Creates a key for the HMAC-SHA256 signature algorithm.
Create a token:
Jwts.builder():
Creates a new JwtBuilder to create a JWT.
setSubject(subject):
Sets the subject of the token.
setIssuedAt(Date()):
Sets the token issue time.
setExpiration(Date(System.currentTimeMillis() + 3600000)):
Sets the token expiration time (1 hour).
signWith(key):
Signs the token using the generated key.
compact():
Finishes the token creation and returns it as a string.
Parsing the token:
Jwts.parserBuilder():
Creates a new JwtParserBuilder for parsing the JWT.
setSigningKey(key):
Sets the key for verifying the token signature.
build():
Finishes building the parser.
parseClaimsJws(token):
Parses and verifies the JWT signature.
body.subject:
Extracts the subject from the token payload.
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController class JwtController { @GetMapping("/generate") fun generateToken(@RequestParam subject: String): String { return JwtUtil.createToken(subject) } @GetMapping("/validate") fun validateToken(@RequestParam token: String): String { return JwtUtil.parseToken(token) ?: "Invalid token" } }
Controller methods:
generateToken:
Handles GET requests to /generate and returns a generated JWT token for the given subject.
validateToken:
Handles GET requests to /validate and validates the passed JWT token, returning the subject or an error message.
Application startup:
When the application starts, Spring Boot automatically configures Spring MVC and creates the routes defined in JwtController. Requests to /generate will create a new JWT token, and requests to /validate will validate the passed token.
ReactiveAuthenticationManager:
This interface is used to validate the user’s identity based on the provided JWT token.
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.server.authentication.ServerAuthenticationConverter import org.springframework.stereotype.Component import reactor.core.publisher.Mono @Component class JwtReactiveAuthenticationManager( private val jwtUtil: JwtUtil, private val userDetailsService: UserDetailsService ) : ReactiveAuthenticationManager { override fun authenticate(authentication: Authentication): Mono<Authentication> { val authToken = authentication.credentials.toString() val username = jwtUtil.extractUsername(authToken) return if (username.isNotEmpty() && jwtUtil.validateToken(authToken)) { val userDetails: UserDetails = userDetailsService.loadUserByUsername(username) val auth = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities) Mono.just(auth) } else { Mono.empty() } } } @Component class JwtUtil(private val secret: String) { fun extractUsername(token: String): String { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).body.subject } fun validateToken(token: String): Boolean { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token) return true } catch (e: Exception) { return false } } }
ServerSecurityContextRepository:
This interface is used to extract the token from the HTTP request and set the security context.
import org.springframework.http.HttpHeaders import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.web.server.context.ServerSecurityContextRepository import org.springframework.stereotype.Component import org.springframework.web.server.ServerWebExchange import reactor.core.publisher.Mono @Component class JwtSecurityContextRepository( private val authenticationManager: ReactiveAuthenticationManager ) : ServerSecurityContextRepository { override fun save(exchange: ServerWebExchange?, context: SecurityContext?): Mono<Void> { // Saving security context is not required return Mono.empty() } override fun load(exchange: ServerWebExchange): Mono<SecurityContext> { val authHeader = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION) return if (authHeader != null && authHeader.startsWith("Bearer ")) { val authToken = authHeader.substring(7) val auth = UsernamePasswordAuthenticationToken(authToken, authToken) authenticationManager.authenticate(auth).map { SecurityContextImpl(it) } } else { Mono.empty() } } }
Now that we have ReactiveAuthenticationManager and ServerSecurityContextRepository, we need to configure security in Spring Security.
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.web.server.SecurityWebFilterChain @Configuration @EnableWebFluxSecurity class SecurityConfig( private val authenticationManager: JwtReactiveAuthenticationManager, private val securityContextRepository: JwtSecurityContextRepository ) { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http .csrf().disable() .authorizeExchange() .pathMatchers("/login", "/register").permitAll() .anyExchange().authenticated() .and() .authenticationManager(authenticationManager) .securityContextRepository(securityContextRepository) .build() } }
ReactiveAuthenticationManager:
JwtReactiveAuthenticationManager implements the ReactiveAuthenticationManager interface.
It extracts the username from the JWT token and checks its validity.
If the token is valid, it returns an authentication object.
ServerSecurityContextRepository:
JwtSecurityContextRepository implements the ServerSecurityContextRepository interface.
It extracts the token from the HTTP Authorization header and authenticates it using the ReactiveAuthenticationManager.
Security Configuration:
The SecurityConfig configuration class configures the security filter chain.
It defines public paths (e.g. login and registration) and protected paths.
It sets up a custom ReactiveAuthenticationManager and ServerSecurityContextRepository.
Another example:
import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys import java.util.* import kotlin.collections.HashMap data class User( val username: String, val role: String ) @Component class JwtUtil { @Value("\${jwt.secret}") lateinit var secret: String @Value("\${jwt.expiration}") lateinit var expirationTime: String fun extractUsername(authToken: String): String { return getClaimsFromToken(authToken).subject } fun getClaimsFromToken(authToken: String): Claims { val key = Base64.getEncoder().encodeToString(secret.toByteArray()) return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(authToken) .body } fun validateToken(authToken: String): Boolean { return getClaimsFromToken(authToken).expiration.after(Date()) } fun generateToken(user: User): String { val claims = HashMap<String, Any>() claims["role"] = listOf(user.role) val expirationSeconds = expirationTime.toLong() val creationDate = Date() val expirationDate = Date(creationDate.time + expirationSeconds * 1000) return Jwts.builder() .setClaims(claims) .setSubject(user.username) .setIssuedAt(creationDate) .setExpiration(expirationDate) .signWith(Keys.hmacShaKeyFor(secret.toByteArray())) .compact() } }
➤ How to use Lombok in Java
Lombok is a library that simplifies the process of writing Java code by providing annotations to automatically generate code such as getters, setters, constructors, and more.
dependencies { compileOnly 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.32' testCompileOnly 'org.projectlombok:lombok:1.18.32' testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' }
@Getter:
Generates a getter for the field.
@Setter:
Generates a setter for the field.
@ToString:
Generates a toString() method that includes all fields of the class.
@EqualsAndHashCode:
Generates equals() and hashCode() methods that include all fields of the class.
@NoArgsConstructor:
Generates a constructor with no arguments.
@RequiredArgsConstructor:
Generates a constructor for fields marked as final or @NonNull.
@AllArgsConstructor:
Generates a constructor with arguments for all fields.
@Data:
Combines multiple annotations: @Getter, @Setter, @ToString, @EqualsAndHashCode, and @RequiredArgsConstructor.
@Value:
Denotes an immutable class, equivalent to a combination of @Getter, @AllArgsConstructor, @ToString, @EqualsAndHashCode, and makes all fields private and final.
@Builder:
Enforces the Builder pattern for the class.
@Slf4j:
Generates a logger for the class using org.slf4j.Logger.
@NonNull:
Checks that a field or method parameter is not null. If null, throws a NullPointerException.
@Synchronized:
Synchronizes a method or block of code, similar to the synchronized keyword, but uses the object’s private monitor.
@Getter(lazy=true):
Creates lazy initialization for a field.
➤ What is Swagger / OpenAPI
OpenAPI, formerly known as Swagger, is a specification for creating, describing, documenting, and consuming RESTful web services. The OpenAPI Specification (OAS) enables developers to automate the processes of API documentation, client and server code generation, and API testing.
An example of an OpenAPI specification:
openapi: 3.0.0 info: title: Simple User API description: A simple API to manage users. version: 1.0.0 servers: - url: http://localhost:8080/api paths: /users: get: summary: Get all users responses: '200': description: A list of users content: application/json: schema: type: array items: $ref: '#/components/schemas/User' post: summary: Create a new user requestBody: description: User to add required: true content: application/json: schema: $ref: '#/components/schemas/User' responses: '201': description: User created /users/{id}: get: summary: Get a user by ID parameters: - name: id in: path required: true schema: type: string responses: '200': description: A single user content: application/json: schema: $ref: '#/components/schemas/User' '404': description: User not found components: schemas: User: type: object properties: id: type: string name: type: string email: type: string
OpenAPI Tools:
Swagger UI:
Generates interactive documentation for APIs based on the OpenAPI specification.
Swagger Editor:
A web-based editor for creating OpenAPI specifications.
Swagger Codegen:
A tool for generating client and server code based on the OpenAPI specification.
OpenAPI Generator:
An alternative code generation tool that supports more languages and platforms.
Integrating OpenAPI into Spring Boot:
You can use Springdoc OpenAPI to integrate OpenAPI into Spring Boot.
dependencies { implementation 'org.springdoc:springdoc-openapi-ui:1.6.14' }
Configuring OpenAPI:
Springdoc OpenAPI automatically generates an OpenAPI specification based on your Spring controllers and models.
import org.springframework.web.bind.annotation.* import java.util.concurrent.ConcurrentHashMap data class User( var id: String, var name: String, var email: String ) @RestController @RequestMapping("/api/users") class UserController { private val users = ConcurrentHashMap<String, User>() @GetMapping fun getAllUsers(): Map<String, User> { return users } @PostMapping fun createUser(@RequestBody user: User): User { users[user.id] = user return user } @GetMapping("/{id}") fun getUserById(@PathVariable id: String): User? { return users[id] } }
Accessing Swagger UI:
Run your Spring Boot application and navigate to http://localhost:8080/swagger-ui.html to see the auto-generated documentation for your API.
➤ How to set up access via OpenVPN
This example will cover setting up OpenVPN on an Ubuntu server.
Installing OpenVPN:
sudo apt update sudo apt install openvpn easy-rsa
Create a directory to store configuration files:
make-cadir ~/openvpn-ca cd ~/openvpn-ca
Set up variables:
nano vars
Change the lines to match your settings:
export KEY_COUNTRY="US" export KEY_PROVINCE="CA" export KEY_CITY="SanFrancisco" export KEY_ORG="MyOrg" export KEY_EMAIL="[email protected]" export KEY_OU="MyOrgUnit"
Generate certificates and keys:
source vars ./clean-all ./build-ca ./build-key-server server ./build-dh openvpn --genkey --secret keys/ta.key
Set up the server configuration file:
nano /etc/openvpn/server.conf
Configuration example:
port 1194 proto udp dev tun ca ca.crt cert server.crt key server.key dh dh2048.pem tls-auth ta.key 0 cipher AES-256-CBC auth SHA256 keepalive 10 120 persist-key persist-tun status /var/log/openvpn/status.log log /var/log/openvpn/openvpn.log verb 3
Start the OpenVPN server:
sudo systemctl start openvpn@server sudo systemctl enable openvpn@server
Setting up the OpenVPN client:
Create a client configuration file that you will use to connect to the VPN.
Sample client configuration file:
client dev tun proto udp remote YOUR_SERVER_IP 1194 resolv-retry infinite nobind persist-key persist-tun remote-cert-tls server cipher AES-256-CBC auth SHA256 key-direction 1 verb 3 <ca> -----BEGIN CERTIFICATE----- # Your CA certificate here -----END CERTIFICATE----- </ca> <cert> -----BEGIN CERTIFICATE----- # Your client certificate here -----END CERTIFICATE----- </cert> <key> -----BEGIN PRIVATE KEY----- # Your client private key here -----END PRIVATE KEY----- </key> <tls-auth> -----BEGIN OpenVPN Static key V1----- # Your TLS key here -----END OpenVPN Static key V1----- </tls-auth>
➤ What is Keycloak
Keycloak is an open-source identity and access management (IAM) system designed to simplify user management, authentication, and authorization in modern applications and services. Keycloak provides various features to ensure the security of applications and services, such as single sign-on (SSO), multi-factor authentication (MFA), social authentication, and more.
First, install Keycloak. You can use Docker for this:
docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin jboss/keycloak
dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.keycloak:keycloak-spring-boot-starter:15.0.2' }
server.port=8081 spring.main.allow-bean-definition-overriding=true keycloak.auth-server-url=http://localhost:8080/auth keycloak.realm=myrealm keycloak.resource=myclient keycloak.public-client=true keycloak.security-constraints[0].authRoles[0]=user keycloak.security-constraints[0].securityCollections[0].patterns[0]=/*
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy @Configuration @EnableWebSecurity class SecurityConfig : KeycloakWebSecurityConfigurerAdapter() { @Autowired fun configureGlobal(auth: AuthenticationManagerBuilder) { auth.authenticationProvider(keycloakAuthenticationProvider()) } @Bean override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy { return NullAuthenticatedSessionStrategy() } @Throws(Exception::class) override fun configure(http: HttpSecurity) { super.configure(http) http.authorizeRequests() .antMatchers("/public*").permitAll() .anyRequest().authenticated() } }
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController class TestController { @GetMapping("/public") fun publicEndpoint(): String { return "This is a public endpoint" } @GetMapping("/secured") fun securedEndpoint(): String { return "This is a secured endpoint" } }
➤ What is Testcontainers
A Java library that simplifies writing integration tests that require working with external systems such as databases, message queues, and other services wrapped in Docker containers. It provides a simple API for managing the lifecycle of Docker containers, making integration tests more isolated, reproducible, and reliable.
Key features of Testcontainers:
Ease of use:
Testcontainers provides a convenient API for starting and stopping Docker containers in tests.
Support for various systems:
Support for containers for various databases (PostgreSQL, MySQL, MongoDB, etc.), queue systems (Kafka, RabbitMQ), and other services.
Container lifecycle:
Automatic management of the container lifecycle, including starting them before a test and stopping them after a test.
Networking:
Support for network modes and configuring network connections between containers.
dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") runtimeOnly("mysql:mysql-connector-java") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.testcontainers:junit-jupiter") testImplementation("org.testcontainers:mysql") }
# application-test.properties spring.datasource.url=jdbc:tc:mysql:latest:///testdb spring.datasource.username=test spring.datasource.password=test spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver spring.jpa.hibernate.ddl-auto=update
import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id @Entity data class User( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, val email: String, val username: String )
import org.springframework.data.jpa.repository.JpaRepository interface UserRepository : JpaRepository<User, Long>
import org.springframework.stereotype.Service @Service class UserService(private val userRepository: UserRepository) { fun createUser(user: User): User { return userRepository.save(user) } fun getUserById(id: Long): User? { return userRepository.findById(id).orElse(null) } fun getAllUsers(): List<User> { return userRepository.findAll() } fun updateUser(id: Long, updatedUser: User): User? { val user = userRepository.findById(id).orElseThrow { RuntimeException("User not found") } user.email = updatedUser.email user.username = updatedUser.username return userRepository.save(user) } fun deleteUser(id: Long) { userRepository.deleteById(id) } }
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.junit.jupiter.SpringExtension import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.containers.MySQLContainer @ExtendWith(SpringExtension::class) @SpringBootTest @Testcontainers class UserServiceTest { @Autowired private lateinit var userService: UserService companion object { @Container val mysqlContainer = MySQLContainer<Nothing>("mysql:latest").apply { withDatabaseName("testdb") withUsername("test") withPassword("test") } } @Test fun `test createUser`() { val user = User(email = "[email protected]", username = "testuser") val savedUser = userService.createUser(user) assertNotNull(savedUser.id) assertEquals("[email protected]", savedUser.email) assertEquals("testuser", savedUser.username) } @Test fun `test getAllUsers`() { val user1 = User(email = "[email protected]", username = "testuser1") val user2 = User(email = "[email protected]", username = "testuser2") userService.createUser(user1) userService.createUser(user2) val users = userService.getAllUsers() assertEquals(2, users.size) } }
@Testcontainers:
Annotation indicating that this test class uses Testcontainers.
@Container:
Indicates that the field is a container that will be automatically managed by Testcontainers.
MySQLContainer:
A Testcontainers class to run a MySQL container.
➤ What is the difference between Jetty, Netty and Tomcat
Jetty, Netty, and Tomcat are three popular servers/containers in the Java world, each with its own features, architecture, and areas of application. Let’s look at the main differences between them:
Jetty:
Purpose:
Jetty is an HTTP server and Servlet container designed for hosting web applications. It supports HTTP/2, WebSocket, and Servlets.
Usage:
Often used in projects that require embedding a server into applications (e.g., Spring Boot).
Suitable for development and testing, as well as for hosting full-fledged web applications.
Performance:
Copes well with handling a large number of simultaneous requests thanks to asynchronous processing. However, it may not be as high-performance and low-level as Netty.
Ease of use:
Easily integrated into projects thanks to good documentation and support in popular frameworks (e.g., Spring Boot).
Key Features:
Supports Servlet 3.1, HTTP/2, WebSocket, and asynchronous IO. Easily embeddable and modular.
Netty:
Purpose:
An asynchronous networking framework designed for building high-performance, scalable network applications. Suitable for implementing low-level network protocols.
Usage:
Widely used to develop servers and clients for high-load systems such as distributed systems, game servers, proxy servers, and databases.
Performance:
High performance due to asynchronous and non-blocking architecture. Optimized to minimize latency and maximize throughput.
Ease of Use:
Requires more low-level programming and knowledge of network protocols. Can be more difficult to learn than Jetty and Tomcat, but provides more control and flexibility.
Key Features:
Supports TCP, UDP, HTTP, WebSocket, and other protocols. Asynchronous and non-blocking IO, NIO support.
Tomcat:
Purpose:
Servlet container and HTTP server designed for hosting Java servlets and JSPs (JavaServer Pages).
Use:
Widely used for hosting web applications, especially those that follow the Java EE specification (servlets and JSPs).
Performance:
Good performance for hosting web applications, but not as optimized for low-level network operations as Netty. Supports asynchronous request processing, but the underlying architecture is more oriented towards traditional web applications.
Ease of Use:
Well documented and supported in most Java development and deployment tools. Easily integrates with Java EE and Spring frameworks.
Key Features:
Supports Servlet 4.0, JSP, WebSocket, and asynchronous IO. Often used in combination with Apache HTTP Server to create hybrid solutions.
Jetty:
A good choice for embedded servers and asynchronous web applications.
Netty:
Optimal for high-performance and low-level network applications.
Tomcat:
Suitable for traditional web applications and applications that comply with the Java EE specification.
➤ What is Netty
An asynchronous Java-based networking framework for developing high-performance, scalable server-side applications and clients, such as network protocols, web servers, proxy servers, databases, and more. It is widely used in the industry due to its high performance and flexibility.
Here is an example of a simple Netty-based Echo server that returns to the client the same messages it received.
dependencies { implementation("io.netty:netty-all:4.1.72.Final") }
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter class EchoServerHandler : ChannelInboundHandlerAdapter() { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { ctx.write(msg) // Write the received message back to the sender ctx.flush() // Flush all previous written messages to the sender } override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { cause.printStackTrace() ctx.close() // Close the connection when an exception is raised } }
import io.netty.bootstrap.ServerBootstrap import io.netty.channel.ChannelFuture import io.netty.channel.ChannelInitializer import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel class EchoServer( private val port: Int ) { fun start() { val bossGroup = NioEventLoopGroup() val workerGroup = NioEventLoopGroup() try { val bootstrap = ServerBootstrap() bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel::class.java) .childHandler(object : ChannelInitializer<SocketChannel>() { @Throws(Exception::class) override fun initChannel(ch: SocketChannel) { ch.pipeline().addLast(EchoServerHandler()) } }) val channelFuture: ChannelFuture = bootstrap.bind(port).sync() channelFuture.channel().closeFuture().sync() } finally { bossGroup.shutdownGracefully() workerGroup.shutdownGracefully() } } } fun main() { val port = 8080 EchoServer(port).start() }
EchoServerHandler:
A class that extends ChannelInboundHandlerAdapter, used to handle incoming messages.
In the channelRead method, the server simply writes and sends back the received message.
The exceptionCaught method handles exceptions that may occur while processing messages.
EchoServer:
A class that represents the server. It uses ServerBootstrap to configure and start the server.
NioEventLoopGroup is used to manage the I/O streams.
ChannelInitializer is used to configure a new channel by adding an instance of EchoServerHandler to its pipeline.
main:
In the main method, an instance of EchoServer is created and started on port 8080.
➤ What is Nginx
Nginx (pronounced “engin-ex”) is a high-performance HTTP server, reverse proxy, and mail (IMAP/POP3) proxy server. It was created by Igor Sysoev and first released in October 2004. Nginx has become very popular due to its high performance, low resource consumption, and flexibility.
Nginx is often used with Spring for various tasks such as reverse proxying, load balancing, and security. In this context, Nginx acts as a frontend server that handles incoming HTTP(S) requests and forwards them to the Spring Boot application server. Here are some examples of how Nginx can be used with Spring Boot:
Example of using Nginx with Spring Boot:
Reverse proxying:
Nginx can serve as a reverse proxy to forward requests to a Spring Boot application running on a different port.
Nginx configuration:
# Nginx configuration file (usually located in /etc/nginx/nginx.conf or /etc/nginx/sites-available/default) server { listen 80; server_name example.com; location / { proxy_pass http://localhost:8080; # URL of your Spring Boot application proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Setting up errors error_page 404 /404.html; location = /404.html { root /var/www/html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /var/www/html; } }
Load Balancing:
If you have multiple instances of a Spring Boot application, Nginx can distribute requests between them, providing load balancing.
Nginx configuration for load balancing:
# Nginx Configuration File upstream spring_app { server localhost:8080; server localhost:8081; server localhost:8082; } server { listen 80; server_name example.com; location / { proxy_pass http://spring_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } error_page 404 /404.html; location = /404.html { root /var/www/html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /var/www/html; } }
SSL/TLS setup:
Nginx can handle SSL/TLS certificates to provide secure connection (HTTPS) for your Spring Boot application.
Nginx configuration with SSL:
# Nginx Configuration File server { listen 80; server_name example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name example.com; ssl_certificate /path/to/your/cert.pem; ssl_certificate_key /path/to/your/cert.key; location / { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } error_page 404 /404.html; location = /404.html { root /var/www/html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /var/www/html; } }
➤ Example of a simple web server
To create the simplest server socket servlet from scratch, you need to implement a server that will listen on a specific port and process HTTP requests. In this example, we will create a server that can process GET and POST requests.
plugins { id 'org.jetbrains.kotlin.jvm' version '1.8.0' id 'application' } repositories { mavenCentral() } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" } application { mainClass = 'SimpleHttpServerKt' }
Using Socket:
import java.io.* import java.net.ServerSocket import java.net.Socket fun main() { val server = SimpleHttpServer() server.start(8080) } class SimpleHttpServer { fun start(port: Int) { val serverSocket = ServerSocket(port) println("Server is listening on port $port") while (true) { val socket = serverSocket.accept() Thread(ClientHandler(socket)).start() } } private class ClientHandler(private val socket: Socket) : Runnable { override fun run() { socket.use { val input = it.getInputStream() val reader = BufferedReader(InputStreamReader(input)) val output = it.getOutputStream() val writer = PrintWriter(output, true) val request = reader.readLine() val tokens = request.split(" ") val method = tokens[0] val path = tokens[1] when (method) { "GET" -> handleGetRequest(writer, path) "POST" -> handlePostRequest(reader, writer, path) else -> { writer.println("HTTP/1.1 405 Method Not Allowed") writer.println("Content-Length: 0") writer.println() } } } } private fun handleGetRequest(writer: PrintWriter, path: String) { writer.println("HTTP/1.1 200 OK") writer.println("Content-Type: text/html") writer.println() writer.println("<html><body>") writer.println("<h1>GET request received</h1>") writer.println("<p>Path: $path</p>") writer.println("</body></html>") } private fun handlePostRequest(reader: BufferedReader, writer: PrintWriter, path: String) { val payload = StringBuilder() var line: String? while (reader.readLine().also { line = it } != "") { payload.append(line).append("\n") } writer.println("HTTP/1.1 200 OK") writer.println("Content-Type: text/html") writer.println() writer.println("<html><body>") writer.println("<h1>POST request received</h1>") writer.println("<p>Path: $path</p>") writer.println("<p>Payload: $payload</p>") writer.println("</body></html>") } } }
Using AsynchronousServerSocketChannel:
import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.AsynchronousServerSocketChannel import java.nio.channels.AsynchronousSocketChannel import java.nio.channels.CompletionHandler import java.nio.charset.Charset fun main() { val server = SimpleAsyncHttpServer() server.start(8080) } class SimpleAsyncHttpServer { fun start(port: Int) { val serverChannel = AsynchronousServerSocketChannel.open() serverChannel.bind(InetSocketAddress(port)) println("Server is listening on port $port") serverChannel.accept(null, object : CompletionHandler<AsynchronousSocketChannel, Void?> { override fun completed(clientChannel: AsynchronousSocketChannel?, attachment: Void?) { serverChannel.accept(null, this) handleClient(clientChannel) } override fun failed(exc: Throwable?, attachment: Void?) { println("Failed to accept connection: ${exc?.message}") } }) // Keep the main thread alive to accept connections Thread.currentThread().join() } private fun handleClient(clientChannel: AsynchronousSocketChannel?) { if (clientChannel != null && clientChannel.isOpen) { val buffer = ByteBuffer.allocate(1024) clientChannel.read(buffer, buffer, object : CompletionHandler<Int, ByteBuffer> { override fun completed(result: Int?, attachment: ByteBuffer?) { if (result != null && result > 0) { attachment?.flip() val request = Charset.defaultCharset().decode(attachment).toString() println("Received request: $request") val response = buildResponse(request) writeResponse(clientChannel, response) attachment?.clear() clientChannel.read(attachment, attachment, this) } else { clientChannel.close() } } override fun failed(exc: Throwable?, attachment: ByteBuffer?) { println("Failed to read from client: ${exc?.message}") clientChannel.close() } }) } } private fun buildResponse(request: String): String { val lines = request.split("\r\n") val requestLine = lines.firstOrNull() val method = requestLine?.split(" ")?.get(0) ?: "" val path = requestLine?.split(" ")?.get(1) ?: "" return when (method) { "GET" -> { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>GET request received</h1><p>Path: $path</p>" } "POST" -> { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>POST request received</h1><p>Path: $path</p>" } else -> { "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n" } } } private fun writeResponse(clientChannel: AsynchronousSocketChannel, response: String) { val buffer = ByteBuffer.wrap(response.toByteArray(Charset.defaultCharset())) clientChannel.write(buffer, buffer, object : CompletionHandler<Int, ByteBuffer> { override fun completed(result: Int?, attachment: ByteBuffer?) { if (attachment?.hasRemaining() == true) { clientChannel.write(attachment, attachment, this) } else { println("Response sent to client") clientChannel.close() } } override fun failed(exc: Throwable?, attachment: ByteBuffer?) { println("Failed to write to client: ${exc?.message}") clientChannel.close() } }) } }