Review
In the previous section, we have laid down the functional specs of the application. In this section, we will discuss the project's structure, write the Java classes, and organize them in layers.Table of Contents
Part 1: Introduction and Functional SpecsPart 2: Java classes
Part 3: XML configuration
Part 4: HTML Files
Part 5: Running the Application
Project Structure
Our application is a Maven project and therefore follows Maven structure. As we create the classes, we've organized them in logical layers: domain, repository, service, and controller.Here's a preview of our project's structure:
The Layers
Domain Layer
This layer contains two domain classes, User and Role. They represent our database tables, user and role respectively. Because we're developing a JPA-based repository, both classes must be annotated with JPA annotations.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.krams.domain; | |
import javax.persistence.CascadeType; | |
import javax.persistence.Column; | |
import javax.persistence.Entity; | |
import javax.persistence.Id; | |
import javax.persistence.OneToOne; | |
@Entity(name="user") | |
public class User { | |
@Id | |
private Long id; | |
private String firstName; | |
private String lastName; | |
@Column(unique=true) | |
private String username; | |
private String password; | |
@OneToOne(mappedBy="user", cascade={CascadeType.ALL}) | |
private Role role; | |
...getters/setters | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.krams.domain; | |
import javax.persistence.Entity; | |
import javax.persistence.Id; | |
import javax.persistence.OneToOne; | |
@Entity(name="role") | |
public class Role { | |
@Id | |
private Long id; | |
@OneToOne | |
private User user; | |
private Integer role; | |
...getters/setters | |
} |
Specifying the entity name inside the @Entity is not required. However, I've encountered instances where the tables are created in lowercase and uppercase, i.e user and USER. For consistency, I've specified them here.
Controller Layer
This layer contains two controllers, AccessController and MediatorController.- AccessController is responsible for handling access related requests, mainly login and logout requests
- MediatorController is responsible for handling requests to common pages such as user and admin pages
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.krams.controller; | |
import org.springframework.stereotype.Controller; | |
import org.springframework.ui.Model; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RequestParam; | |
@Controller | |
@RequestMapping | |
public class AccessController { | |
@RequestMapping("/login") | |
public String login(Model model, @RequestParam(required=false) String message) { | |
model.addAttribute("message", message); | |
return "access/login"; | |
} | |
@RequestMapping(value = "/denied") | |
public String denied() { | |
return "access/denied"; | |
} | |
@RequestMapping(value = "/login/failure") | |
public String loginFailure() { | |
String message = "Login Failure!"; | |
return "redirect:/login?message="+message; | |
} | |
@RequestMapping(value = "/logout/success") | |
public String logoutSuccess() { | |
String message = "Logout Success!"; | |
return "redirect:/login?message="+message; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.krams.controller; | |
import org.springframework.stereotype.Controller; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
@Controller | |
@RequestMapping("/") | |
public class MediatorController { | |
@RequestMapping | |
public String getHomePage() { | |
return "home"; | |
} | |
@RequestMapping(value="/user") | |
public String getUserPage() { | |
return "user"; | |
} | |
@RequestMapping(value="/admin") | |
public String getAdminPage() { | |
return "admin"; | |
} | |
} |
Service Layer
This layer contains a single service, CustomUserDetailsService. Its main purpose is to retrieve user information from our custom database and translate that user information into a format that Spring Security understands. Take note of the helper methods provided within this class.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.krams.service; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.List; | |
import org.krams.repository.UserRepository; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.security.core.GrantedAuthority; | |
import org.springframework.security.core.authority.SimpleGrantedAuthority; | |
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.core.userdetails.UsernameNotFoundException; | |
import org.springframework.stereotype.Service; | |
import org.springframework.transaction.annotation.Transactional; | |
/** | |
* A custom {@link UserDetailsService} where user information | |
* is retrieved from a JPA repository | |
*/ | |
@Service | |
@Transactional(readOnly = true) | |
public class CustomUserDetailsService implements UserDetailsService { | |
@Autowired | |
private UserRepository userRepository; | |
/** | |
* Returns a populated {@link UserDetails} object. | |
* The username is first retrieved from the database and then mapped to | |
* a {@link UserDetails} object. | |
*/ | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
try { | |
org.krams.domain.User domainUser = userRepository.findByUsername(username); | |
boolean enabled = true; | |
boolean accountNonExpired = true; | |
boolean credentialsNonExpired = true; | |
boolean accountNonLocked = true; | |
return new User( | |
domainUser.getUsername(), | |
domainUser.getPassword().toLowerCase(), | |
enabled, | |
accountNonExpired, | |
credentialsNonExpired, | |
accountNonLocked, | |
getAuthorities(domainUser.getRole().getRole())); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
/** | |
* Retrieves a collection of {@link GrantedAuthority} based on a numerical role | |
* @param role the numerical role | |
* @return a collection of {@link GrantedAuthority | |
*/ | |
public Collection<? extends GrantedAuthority> getAuthorities(Integer role) { | |
List<GrantedAuthority> authList = getGrantedAuthorities(getRoles(role)); | |
return authList; | |
} | |
/** | |
* Converts a numerical role to an equivalent list of roles | |
* @param role the numerical role | |
* @return list of roles as as a list of {@link String} | |
*/ | |
public List<String> getRoles(Integer role) { | |
List<String> roles = new ArrayList<String>(); | |
if (role.intValue() == 1) { | |
roles.add("ROLE_USER"); | |
roles.add("ROLE_ADMIN"); | |
} else if (role.intValue() == 2) { | |
roles.add("ROLE_USER"); | |
} | |
return roles; | |
} | |
/** | |
* Wraps {@link String} roles to {@link SimpleGrantedAuthority} objects | |
* @param roles {@link String} of roles | |
* @return list of granted authorities | |
*/ | |
public static List<GrantedAuthority> getGrantedAuthorities(List<String> roles) { | |
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); | |
for (String role : roles) { | |
authorities.add(new SimpleGrantedAuthority(role)); | |
} | |
return authorities; | |
} | |
} |
What's the logic here?
- We must implement UserDetailsService because we have a custom database
- loadUserByUsername method must return an object that implements the UserDetails interface
- We must map our domain org.krams.domain.User to org.springframework.security.core.userdetails.User
- We must map numerical roles as SimpleGrantedAuthority objects
- We are responsible for interpreting what each numerical role value represents
- ROLE_USER and ROLE_ADMIN are abitrary values we assigned to numerical values
Repository Layer
This layer contains a single interface, UserRepository. And this is our data access object (DAO). With the help of Spring Data JPA, Spring will automatically provide the actual implementation.What is Spring Data JPA?
Spring JPA is part of the umbrella Spring Data project that makes it easy to easily implement JPA based repositories.
Implementing a data access layer of an application has been cumbersome for quite a while. Too much boilerplate code has to be written to execute simple queries as well as perform pagination, and auditing. Spring JPA aims to significantly improve the implementation of data access layers by reducing the effort to the amount that's actually needed. As a developer you write your repository interfaces, including custom finder methods, and Spring will provide the implementation automatically.
Source: http://www.springsource.org/spring-data/jpa
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.krams.repository; | |
import org.krams.domain.User; | |
import org.springframework.data.jpa.repository.JpaRepository; | |
public interface UserRepository extends JpaRepository<User, Long> { | |
User findByUsername(String username); | |
} |
Utility classes
TraceInterceptor class is an AOP-based utility class to help us debug our application. This is a subclass of CustomizableTraceInterceptor (see Spring Data JPA FAQ)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.krams.aop; | |
import org.aopalliance.intercept.MethodInvocation; | |
import org.apache.commons.logging.Log; | |
import org.apache.log4j.Logger; | |
import org.springframework.aop.interceptor.CustomizableTraceInterceptor; | |
/** | |
* Extends {@link CustomizableTraceInterceptor} to provide custom logging levels | |
*/ | |
public class TraceInterceptor extends CustomizableTraceInterceptor { | |
private static final long serialVersionUID = 287162721460370957L; | |
protected static Logger logger4J = Logger.getLogger("aop"); | |
@Override | |
protected void writeToLog(Log logger, String message, Throwable ex) { | |
if (ex != null) { | |
logger4J.debug(message, ex); | |
} else { | |
logger4J.debug(message); | |
} | |
} | |
@Override | |
protected boolean isInterceptorEnabled(MethodInvocation invocation, Log logger) { | |
return true; | |
} | |
} |
Next
In the next section, we will focus on the configuration files and create them accordingly. Click here to proceed.
Share the joy:
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |

