A Comprehensive Example of a Spring MVC Application - Part 2

The Service Layer

The service layer is the heart of the application. All the business logic should go here. Unlike repositories, services may call each other because they are in the same layer. Each service should encapsulate a logical collection of transactions. Most services will use repositories because most services do things that are related to the database. However, not all service methods are required to use a repository. On the other hand - all public service methods must define @Transactional because as an architecture decision the transaction starts and ends at the service layer.

 

A service should have an interface and an implementation class. This is convenient if your application has multiple modules. When separating the services into interfaces and implementation classes you can put all interfaces in a dedicated module (called API) and all implementation classes in their own module (impl). This way when you want to reuse services or access one service from another you can depend on the API module only - avoiding cyclic dependencies. The implementation modules must never depend on one another.

 

Implementing the Service Layer

The first thing to do here is to define the transaction manager. We need a JPA transaction manager because we are using Hibernate Entity Manager and enable the annotation driven transactions:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
          p:entityManagerFactory-ref="emf"
          p:dataSource-ref="dataSource"/>

    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

 Now we need to scan all our @Service annotated classes:

<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
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.hp.example.services"/>
</beans>

 

In the application context we only want to scan the services. We do not want to scan web layer beans such as controllers and view related beans - these must only exist in the web layer.

 

And now the PersonService interface:

public interface PersonService {
    List<Person> readAllPersons();

    long createPerson(String firstName, String lastName);

    Person readPerson(long id);

    void deletePerson(long id);
}

 

And the PersonServiceImpl class:

// the service is final because there's no reason to ever extend it.Polymorphism should only be used in the model
@Service
public final class PersonServiceImpl implements PersonService {
    @Autowired
    private PersonRepository personRepository;

    @Override
    @Transactional(readOnly = true)    // notice the read only flag - this is an important optimization for read methods
    public List<Person> readAllPersons() {
        return personRepository.findAll();
    }

    @Override
    @Transactional
    public long createPerson(String firstName, String lastName) {
        return personRepository.save(new Person(firstName, lastName)).getId();
    }

    @Override
    @Transactional(readOnly = true)
    public Person readPerson(long id) {
        return personRepository.findOne(id);
    }

    @Override
    @Transactional
    public void deletePerson(long id) {
        personRepository.delete(id);
    }
}

 

This service is very simply. It basically just delegates to the repository. In real applications there would be real business logic here.

 

Notice that the read methods use the "read only" flag in the @Transactional annotation - this is an important optimization for read methods. When Spring starts a transaction in "read only" mode it sets Hibernate's flush mode to "never". Doing so makes any change within this transaction to no be persisted to the database, and as a result boosts Hibernate performance.

 

Testing the Service Layer

When testing this layer we don't want to test the DAL. We always want our unit tests to be as precise as possible and test exactly what we need without being affected by external changes. For this reason we'll mock the PersonRepository. I recommend mocking with Mockito - it is a very flexible mocking library: https://code.google.com/p/mockito/.

 

Other than that the service layer test is very straight forward:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class PersonServiceTest {
    @Autowired
    private PersonService personService;
    @Autowired
    private PersonRepository personRepository;

    @Test
    public void wiring() {
        assertThat("personService exists", personService, is(notNullValue()));
    }

    @Test
    public void readAllPersons() {
        when(personRepository.findAll()).thenReturn(Arrays.asList(new Person("George", "Washington")));

        List<Person> persons = personService.readAllPersons();
        assertThat("the service returned a null list", persons, is(notNullValue()));
        assertThat("the service didn't return any persons", persons.size(), is(greaterThan(0)));

        Person mosheCohen = new Person();
        mosheCohen.setFirstName("George");
        mosheCohen.setLastName("Washington");

        assertThat("Moshe Cohen is not in the list", mosheCohen, isIn(persons));
    }

    @Test
    public void createPerson() {
        final List<Person> personsDb = new ArrayList<>(2);
        personsDb.add(new Person("George", "Washington"));

        when(personRepository.findAll()).thenReturn(personsDb);

        List<Person> persons = personService.readAllPersons();
        assertThat("the service returned a null list", persons, is(notNullValue()));
        assertThat("the service didn't return any persons", persons.size(), is(greaterThan(0)));

        int beforeSize = persons.size();

        when(personRepository.save((Person) anyObject())).thenAnswer(new Answer<Person>() {
            @Override
            public Person answer(InvocationOnMock invocationOnMock) throws Throwable {
                // fake a database - add the created person to a list
                Person p = (Person) invocationOnMock.getArguments()[0];
                personsDb.add(p);
                Field f = Person.class.getDeclaredField("id");
                f.setAccessible(true);
                f.set(p, (long) personsDb.size());
                return p;
            }
        });

        long id = personService.createPerson("Elizabeth", "Windsor");
        assertThat("zero ID is unacceptable", id, is(greaterThan(0l)));
        assertThat("there aren't enough persons", personService.readAllPersons().size(), is(greaterThan(beforeSize)));
    }

    @Test
    public void readPerson() {
        when(personRepository.findOne(anyLong())).thenReturn(new Person("George", "Washington"));
        assertThat("couldn't read person 1", personService.readPerson(1l), is(notNullValue()));
    }

    @Configuration
    public static class PersonServiceTestConfiguration {
        @Bean
        public PersonService personService() {
            return new PersonServiceImpl();
        }

        @Bean
        public PersonRepository personRepository() {
            return mock(PersonRepository.class);
        }
    }
}

 Next blog entries:

- The Web Layer

- Testing the Web Layer

Leave a Comment

We encourage you to share your comments on this post. Comments are moderated and will be reviewed
and posted as promptly as possible during regular business hours

To ensure your comment is published, be sure to follow the Community Guidelines.

Be sure to enter a unique name. You can't reuse a name that's already in use.
Be sure to enter a unique email address. You can't reuse an email address that's already in use.
Type the characters you see in the picture above.Type the words you hear.
Search
Showing results for 
Search instead for 
Do you mean 
About the Author


Follow Us
The opinions expressed above are the personal opinions of the authors, not of HP. By using this site, you accept the Terms of Use and Rules of Participation