In this repository, you can learn Spring Rest API from beginner to advanced level.
- Creating Project
- CRUD with H2 Database
- Using MySQL database instead of H2
- One to Many Relation in Hibernate
- Many to Many Relation in Hibernate
- Error Handling
- JPA Hibernate Validations
- Creating Filters
- Spring Security
- Jwt Authentication
In IntelliJ IDEA, go to spring initilizer, create new project by selecting Spring web in dependencies. (referance commit)
Give it proper name at create it, it may take few minutes for downloading the dependencies.
Create a package for controllers in src>main>java>[your pakage name]/controller (referance commit)
Add a controller to display hello world message. @RestController makes it restfull controller and @RequestMapping(value = "/hello") defines URL mapping
@RestController
public class HelloController {
@RequestMapping(value = "/hello")
public String sayHello(){
return "Hello World!";
}
}
to use database we need few dependencies to be installed Go to mavenrepository and search for the following dependencies and add it to your pom.xml dependencies section Please donot include test scope as we will not be doing testing at that stage (referance commit)
Now you have added a database named H2 database(in-memory db), you need to specify database name too- Open application.properties in resources folder
- specify database name as follows
spring.datasource.url=jdbc:h2:~/test;DB_CLOSE_ON_EXIT=FALSE
spring.jpa.hibernate.ddl-auto=update
Create a package entity where you will create Entity classes e.g we are creating Person entity (referance commit)
Use @Entity annotation to make it entity, @Data to make setter getters, @NoArgsConstructor to create no argument constructor, @AllArgsConstructor to make argument constructor. @Id to make it primary key, @GeneratedValue(strategy = GenerationType.AUTO) for autoincrement.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int ID;
private String Name;
private int Age;
private double Height;
private String CNIC;
}
Now your table will be created once the application starts.
Now create a repository for every entity, create package named repository and create interface,, e.g. PersonRepository that will extend JpaRepository<Person,Integer> and use @Repository annotation on it. (referance commit)
@Repository
public interface PersonRepository extends JpaRepository<Person,Integer> {
}
Services will contain business logic, e.g. CRUD operation in this case. Create package service and create service for every repository. e.g. PersonService
Use @Service annotation on PersonService. In PersonService, create private object of PersonRepository and use @Autowired annotation on it, so spring framework will initilize that object. (referance commit)
Now in the controller package, create PersonController that will manage Http requests. Use @RestController, @RequestMapping(value = "/person") as we do in controllers. Create an object of PersonService in PersonController and use @Autowired annotation on it, so spring framework will manage object creation.
Now create GET, POST, PUT and DELETE methods with @GetMapping,@PostMapping, @PutMapping(value = "/{id}") and @DeleteMapping(value = "/{id}"). In the function parameters, use @PathVariable int id to get data from URL like localhost/person/1, and if we use @RequestParam it would be like localhost/person?id=1 and @RequestBody Person person to get data from body. (referance commit)
Adding
@CrossOrigin(origins = "*", allowedHeaders = "*")
on controller so it can be accessed from anywhere. (referance commit)
Adding @JsonProperty on entity to will help you to change JSON object name (referance commit)
Remove H2 Database dependency from pom.xml and add MySQL Connector
comment/remove the H2 database configuration from application.properties file and add MySQL properties as follows: (referance commit)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5Dialect
lets create two Entities Country and City for oneToMany Relationship. Create entity City with properties cityId and cityName annotate them with proper annotations like @Id, @GeneratedValue(strategy = GenerationType.AUTO) and @Column(name = "cityId") and also annotate the table with annotations like @Entity, @Data, @NoArgsConstructor and @AllArgsConstructor.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class City {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "cityId")
private int cityId;
private String cityName;
}
Now create an entity Country, annotate class with annotations @Entity, @Data, @NoArgsConstructor and @AllArgsConstructor, and create the following fields (referance commit)
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Country {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "countryId")
private int countryId;
private String countryName;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "cc_fk",referencedColumnName = "countryId")
private List<City> cities;
}
Annotate it with @OneToMany(cascade = CascadeType.ALL) and @JoinColumn(name = "cc_fk",referencedColumnName = "countryId"). Yhis will create cc_fk column in City table and make it foreign key.
same as above (referance commit)
same as above (referance commit)
same as above (referance commit)
Lets take the example of two entities Student and Course, One student can be enrolled in multiple courses, similarly, Each course contains multiple students. If you remember database normalization rules, we need to break the entity to seperate entity for Many to Many relation.
One ony below table, use @JoinTable annotation to specify 3rd table name, column to join from current table, and column from the other table.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int studentId;
private String name;
private String regNo;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "student_courses",
joinColumns = {@JoinColumn(name = "studentId")},
inverseJoinColumns = {@JoinColumn(name = "courseId")})
private List<Course> courses;
}
and
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int courseId;
private String courseTitle;
private String courseCode;
@ManyToMany
private List<Student> students;
}
Suppose users requests a resource by FindbyId, currently it returns null, instead of null, we will now handle the error and return not found error. For that create a package named exception and create a class to handle exception. (referance commit)
@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
public NotFoundException(String message){
super(message);
}
}
modify getPerson method to throw notfound error
public Person getPerson(int id){
Optional<Person> person = personRepository.findById(id);
if(!person.isPresent()){
throw new NotFoundException("Person not found!");
}
return person.get();
}
To customize table design, we can use different annotatons on top of each field.
- @Id makes it Primary Key.
- @GeneratedValue(strategy = GenerationType.IDENTITY) make it autoincrement by 1.
- @JsonProperty(value = "ID") make sures it should be ID in JSON format instead of id
- @NotNull(message = "Name cann't be null") makes the field non nullable.
- @Size(min = 2,max = 100, message = "Name must be minimum 2 characters and maximum 100 characters long") validates the size of the field.
- @Email is used to validate email.
- @Min(8) and @Max(110) says that number should be between 8-110.
- @Pattern(regexp = "^\(?(\d{5})\)?[-]?(\d{7})[-]?(\d{1})$",message = "CNIC sholud be in format xxxxx-xxxxxxx-x") is used for regular expression.
- @Past makes sure that date should be from past, mnot future.
- @JsonFormat(pattern = "yyyy-MM-dd") make sures JSON data should be in this format.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonProperty(value = "ID")
private int id;
@JsonProperty(value = "Name")
@NotNull(message = "Name cann't be null")
@Size(min = 2,max = 100, message = "Name must be minimum 2 characters and maximum 100 characters long")
private String name;
@Email
private String email;
@JsonProperty(value = "Age")
@Min(8)
@Max(110)
private int age;
@JsonProperty(value = "Height")
private double height;
@JsonProperty(value = "CNIC")
@Pattern(regexp = "^\\(?(\\d{5})\\)?[-]?(\\d{7})[-]?(\\d{1})$",message = "CNIC sholud be in format xxxxx-xxxxxxx-x")
private String cnic;
@Past
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate doB;
}
After adding these annotations, our table will be created with those restrictions. But if we insert invalid data through JSON, our application will throw exception or maybe crash. We need to validate these properties in controller to avoid exceptions.
Use @Valid to check for validations, and also inject BindingResult in it which will help us to catch errors, If invalid, then return errors, else, return created object. Simplarly check for validations in PutMapping function too.
@PostMapping
public ResponseEntity<?> addPerson(@Valid @RequestBody Person person, BindingResult result){
if(result.hasErrors()){
Map<String,String> errors = new HashMap<>();
for(FieldError error:result.getFieldErrors()){
errors.put(error.getField(),error.getDefaultMessage());
}
return new ResponseEntity<Map<String,String>>(errors, HttpStatus.BAD_REQUEST);
}
Person p = personService.addPerson(person);
return new ResponseEntity<Person>(p,HttpStatus.CREATED);
}
Create a package filters and create filter in it. Make it component of spring framework by adding @Component annotation. By default, filter is called on every url pattren. (referance commit)
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
@Component
public class MyFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
System.out.println("Filter Called");
chain.doFilter(req, resp);
}
}
To map filter on specific URL pattren, we need to do configuration. create pakage config and create class that will map URL pattren as follows: (referance commit)
@Configuration
public class MyFilterConfig {
@Bean
public FilterRegistrationBean<MyFilter> registrationBean(){
FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new MyFilter());
registrationBean.addUrlPatterns("/person/*");
return registrationBean;
}
}
To implement spring security, First we need to add Spring Web Security from maven repository
Now we should have a table in database to register and authenticate users.
Lets create a table/entity for our application users. e.g. ApplicationUser
@Entity
@Table(name = "user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@NotBlank(message = "username cann't be blank")
@Column(unique = true,nullable = false)
private String username;
@Column(nullable = false)
private String password;
}
Add an extra method findByUsername which will help us to find user by their username.
@Repository
public interface UserRepository extends JpaRepository<ApplicationUser,Integer> {
ApplicationUser findByUsername(String username);
}
Now create a service that should implement UserDetailsService as follows
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ApplicationUser user = userRepository.findByUsername(username);
return new User(user.getUsername(),user.getPassword(),new ArrayList<>());
}
}
where User is from import org.springframework.security.core.userdetails.User;
following import.
Now create a configure class that will extend WebSecurityConfigurerAdapter and tell spring security to use our own userdetails service.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Now application will use our own customized security
(Optional) Suppose we want to new user data into database when the application starts we can create new method in main class and use @PostConstruct annotation to bind it with application start.
@Autowired
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@PostConstruct
public void seedUser() {
if (userRepository.findByUsername("test") == null) {
String encodedPassword = passwordEncoder.encode("12345");
ApplicationUser user = new ApplicationUser(1, "test", encodedPassword);
userRepository.save(user);
}
}
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
Its on one of the best way of authentication of modern time.
To include Jwt in your project, you need to include the following dependencies:
you need to include JAXB API if you are using Java 9 or above.
Now create a JwtUtilService that will contains business logic needed by Jwt. Copy it as given below
@Service
public class JwtUtilService {
private final String secret = "fawad";
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, secret).compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Now lets create a controller that will be responsible for creating User account and generate Jwt Token.
Before creating token you need to add one more method in your SecurityConfig class for AuthenticationManager, as we will be using it in our controller.
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
add it in SecurityConfig class.
Now lets create a controller to generate token
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtilService jwtUtil;
@Autowired
private UserService userService;
@PostMapping("/login")
public String login(@RequestBody ApplicationUser userCredentials) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(userCredentials.getUsername(),userCredentials.getPassword())
);
}catch (Exception e){ throw new Exception("Invalid Credentials");}
return jwtUtil.generateToken(userCredentials.getUsername());
}
}
As authorization is applied to every page, but we want that there should be no authorization on AccountController so we can register or login user, to do that, you need to modify SecurityConfig file.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/account/**").permitAll()
.anyRequest().authenticated();
}
Now lets create a filter that will extend OncePerRequestFilter to verify token.
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtUtilService jwtUtil;
@Autowired
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = httpServletRequest.getHeader("Authorization");
//eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmYXdhZDE5OTciLCJleHAiOjE1ODcxNTA2NjgsImlhdCI6MTU4NzE0ODg2OH0.OEvx_a1nTW2vzH7ofuDXJHL8By_32_D3OIfycBoXykY
String token = null;
String username = null;
if(authorizationHeader!=null && authorizationHeader.startsWith("Bearer")){
token = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(token);
}
if(username!=null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = userService.loadUserByUsername(username);
if(jwtUtil.validateToken(token,userDetails)){
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,null,userDetails.getAuthorities());
authenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)
);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
and in the Security config, register filter and stateless session. Inject JwtFilter using @Autowired and update the configure method.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/account/**").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling()
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
Create a method in UserService that will contain business logic of user creation.
@Autowired
private BCryptPasswordEncoder passwordEncoder;
public Boolean registerUser(ApplicationUser user){
String passwordHash = passwordEncoder.encode(user.getPassword());
user.setPassword(passwordHash);
userRepository.save(user);
return true;
}
Now create a method in controller to register user.
@PostMapping("/register")
public String register(@RequestBody ApplicationUser user){
if(userService.registerUser(user)){
return "User Created";
}
return "User Creation Failed!";
}