Java REST at the HeadHunter School of Programmers

  • Tutorial

Hi Habr, we want to tell about one of the projects of the school of programmers HeadHunter 2018. Below is an article of our graduate, in which he will tell about the experience gained during training.



Hello. This year I graduated from the School of Programmers hhand in this post I will tell about the educational project in which I participated. During my school years, and especially on the project, I lacked an example of a combat application (and even better a guide) in which I could see how to properly divide logic and build a scalable architecture. All the articles I found were difficult to understand for a beginner, because either they actively used IoC without exhaustive explanations of how to add new components or modify old ones, or they were archaic and contained a ton of configs on xml and frontend on jsp. I tried to focus on my level before learning, i.e. almost zero with a few reservations, so this article should be useful for future students of the school, as well as self-taught enthusiasts who have decided to start writing in java.


Given (problem statement)


Team - 5 people. Term - 3 months, at the end of each - a demo. The goal is to make an application that helps HR accompany employees on a trial period, automating all the processes that will work out. At the entrance, they explained to us how the probationary period (IS) is arranged: as soon as it becomes known that a new employee is coming out, HR begins to kick the future leader so that he sets tasks for the IP, and this should be done before the first working day. On the day when the employee comes to work, HR conducts a welcome meeting, talks about the company's infrastructure, and handles tasks to the IS. After 1.5 and 3 months, an intermediate and final meeting of the HR, a manager and an employee are held, at which progress is discussed and a form of the results is drawn up. In case of success,


Design


We decided to make for each employee a personal page on which general information will be displayed (name, department, manager, etc.), a field for comments and a history of changes, attached files (tasks on the IP, questionnaire) and employee’s workflow reflecting level of passage of IP. Vorkflou was decided to split into 8 stages, namely:


  • 1st stage - adding an employee: becomes completed immediately after a new employee is registered in the HR system. At the same time, three calendars are sent to HRU for well-being, intermediate and final meeting.
  • Stage 2 - coordination of tasks on the IS: the head is sent a form for setting tasks on the IS, which after filling out will receive HR. HR then prints them out, signs them and marks the end of the stage in the interface.
  • Stage 3 - welcome meeting: HR holds a meeting and presses the “Stage completed” button.
  • 4th stage - intermediate meeting: similar to the third stage
  • Stage 5 - the results of the interim meeting: HR fills in the results on the employee’s page and clicks Next.
  • 6th stage - final meeting: similar to the third stage
  • 7th stage - the results of the final meeting: similar to the fifth stage
  • The 8th stage is the completion of the IP: in case of successful completion of the IP, a link with the form of the questionnaire is sent to the employee by e-mail, and jira automatically creates a task for the design of the VHI (we were given the task before us).

All stages have time after which the stage is considered overdue and is highlighted in red, and a notification arrives by mail. The end time must be editable, for example, in case an interim meeting falls on a holiday or due to some circumstances it is necessary to postpone the meeting.
Unfortunately, the prototypes painted on the leaflets / boards have not been preserved, but at the end there will be screenshots of the finished application.


Exploitation


One of the goals of the school is to prepare students for work in large projects, so the process of releasing tasks was appropriate for us.
Upon completion of the work on the task, we give it to the review_1 to another student from the team to correct obvious errors / exchange of experience. Then there is a review_2 - a task is checked by two mentors, who make sure that we don’t let govnod for a couple with a reviewer_1. Further testing was supposed, but this stage is not very appropriate, given the scale of the school project. So after passing the review, we thought that the task was ready for release.
Now a few words about the warmth. The application must always be available on the network from any computers. To do this, we bought a cheap virtual machine (for 100 rubles per month), but, as I learned later, everything could be arranged for free and in a fashionable way in the AWS docker. For continuous integration, we have chosen Travis. If someone does not know (personally, I never heard of continuous integration before school), it’s such a cool thing that will monitor your github and when a new commit appears (as configured), collect the code in jar, send it to the server and restart the application automatically. How exactly to build, is described in the Travis yamle in the root of the project, it is quite similar to bash, so I think no comments will be required. We also bought the domain www.adaptation.host, so as not to register an ugly IP in the address bar on the demo. We also set up postfix (for sending mail), apache (not nginx, since apache was out of the box) and jira (trial) server. The frontend and backend were made by two separate services that will communicate over http (# 2-18, # microservices). This part of the article "at the school headhunter programmers" smoothly ends, and we turn to the java rest service.


Backend


0. Introduction


We used the following technologies:


  • JDK 1.8;
  • Maven 3.5.2;
  • Postgres 9.6;
  • Hibernate 5.2.10;
  • Jetty 9.4.8;
  • Jersey 2.27.

As a framework, we took NaB 3.5.0 from hh. Firstly, it is used in HeadHunter, and secondly, it contains jetty, jersey, hibernate, embedded postgres out of the box, which is written on the githabe. Let me briefly clarify for beginners: jetty is a web server that identifies clients and organizes sessions for each of them; jersey - a framework that helps to conveniently create a RESTful service; hibernate - ORM to simplify work with the database; maven is a java project builder.
I will show a simple example of how to work with it. I created a small test repositoryIn which he added two entities: a user and a resume, as well as resources for creating and receiving them with the OneToMany / ManyToOne connection. To launch it is enough to clone the repository and run mvn clean install exec: java in the project root. Before commenting on the code, I’ll tell you about the structure of our service. It looks like this:



Main directories:


  • Services is the main directory in the application; all business logic is stored here. In other places working with data without good reason should not be.
  • Resources - URL handlers, the layer between services and the frontend. Here validation of incoming data and conversion of outgoing data are allowed, but not business logic.
  • Dao (Data Access Object) - the layer between the base and services. Tao must contain only fundamental basic operations: add, read, update, delete one / all.
  • Entity - objects that ORM exchanges with the base. As a rule, they correspond directly to the tables and must contain all the fields as the entity in the database with the corresponding types.
  • Dto (Data Transfer Object) is an analogue of an entity, only for resources (front), helps to generate json from the data we want to send / receive.

1. Base


I should use the installed postgres side by side, as in the main application, but I wanted the test case to be simple and run with one command, so I took the built-in HSQLDB. Connecting the database to our infrastructure is done by adding a DataSource to ProdConfig (also remember to tell hibernate which database you are using):


@Bean(destroyMethod = "shutdown")
DataSource dataSource(){
  returnnew EmbeddedDatabaseBuilder()
    .setType(EmbeddedDatabaseType.HSQL)
    .addScript("db/sql/create-db.sql")
    .build();
}

I created the table creation script in the create-db.sql file. You can add other scripts that initialize the database. In our lightweight example with in_memory base, it was possible to do without scripts at all. If you specify in the hibernate.properties settings hibernate.hbm2ddl.auto=create, then hibernate will create tables by entity itself when the application starts. But if you need to have something in the database that is not in the entity, then you cannot do without a file. Personally, I am used to separating the base and the application, so I usually don’t trust hibernate to do such things.
db/sql/create-db.sql:


CREATETABLE employee
(
  idINTEGERIDENTITY PRIMARY KEY,
  first_name      VARCHAR(256) NOTNULL,
  last_name       VARCHAR(256) NOTNULL,
  email           VARCHAR(128) NOTNULL
);
CREATETABLEresume
(
  idINTEGERIDENTITY PRIMARY KEY,
  employee_id     INTEGERNOTNULL,
  positionVARCHAR(128) NOTNULL,
  about            VARCHAR(256) NOTNULL,
  FOREIGNKEY (employee_id) REFERENCES employee(id)
);

2. Entity


entities/employee:


@Entity@Table(name = "employee")publicclassEmployee{
  @Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(name = "id", nullable = false)private Integer id;
  @Column(name = "first_name", nullable = false)private String firstName;
  @Column(name = "last_name", nullable = false)private String lastName;
  @Column(name = "email", nullable = false)private String email;
  @OneToMany(mappedBy = "employee")@OrderBy("id")private List<Resume> resumes;
  //..geters and seters..
}

entities/resume:


@Entity@Table(name = "resume")publicclassResume{
  @Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Integer id;
  @ManyToOne(fetch = FetchType.LAZY)@JoinColumn(name = "employee_id")private Employee employee;
  @Column(name = "position", nullable = false)private String position;
  @Column(name = "about")private String about;
  //..geters and seters..
}

Entities refer to each other not by the class field, but entirely by the parent / heir object. Thus, we can get a recursion when we try to take from the Employee database, for which we’ve got a resume, for which ... To prevent this from happening, we specified annotations @OneToMany(mappedBy = "employee")and @ManyToOne(fetch = FetchType.LAZY). They will be taken into account in the service, when performing a transaction to read / write from the database. Setup is FetchType.LAZYnot necessary, but using lazy communication makes the transaction easier. So, if in a transaction we receive a resume from the database and do not contact the owner, then the employee entity will not be loaded. You can see for yourself: removeFetchType.LAZYand see in debug what comes back from the service along with the resume. But you should be careful - if we did not load the employee into the transaction, then accessing the employee fields outside the transaction can trigger LazyInitializationException.


3. Dao


In our case, EmployeeDao and ResumeDao are almost identical, so here’s just one of them
EmployeeDao:


publicclassEmployeeDao{
  privatefinal SessionFactory sessionFactory;
  @InjectpublicEmployeeDao(SessionFactory sessionFactory){
    this.sessionFactory = sessionFactory;
  }
  publicvoidsave(Employee employee){
    sessionFactory.getCurrentSession().save(employee);
  }
  public Employee getById(Integer id){
    return sessionFactory.getCurrentSession().get(Employee.class, id);
  }
}

Annotation @Injectmeans that in the constructor of our dao, Dependency Injection is used. In my past life, a physicist who has parsed a file, built graphs based on the results of a number and, at the very least, figured out the OOP, in java guides such constructions seemed something insane. And at school, perhaps, this topic is the most non-obvious, IMHO. Fortunately, there are a lot of materials about DI on the Internet. If you are too lazy to read, then the first month you can follow the rule: register new resources / services / dao in our context-config , add the entity to the mapping. If there is a need to use some services / dao in others, they need to be added to the designer with the inject annotation, as shown above, and the spring initializes everything for you. But then you still have to deal with DI.


4. Dto


Dto, like dao, is almost identical for employee and resume. Consider here only employeeDto. We will need two classes: EmployeeCreateDtorequired when creating an employee; EmployeeDtoused when receiving (contains additional fields idand resumes). The field is idadded so that in the future, on requests from the outside, we can work with the employee without conducting a preliminary search for the entity by email. A field resumesto receive an employee along with all his resumes in a single request. It would be possible to manage with one dto for all operations, but then for the list of all resumes of a specific employee we would have to create an additional resource, like getResumesByEmployeeEmail, pollute the code with custom queries to the database and cross out all the conveniences provided by ORM.
EmployeeCreateDto:


publicclass EmployeeCreateDto {
  publicString firstName;
  publicString lastName;
  publicString email;
}

EmployeeDto:


publicclass EmployeeDto {
  public Integer id;
  publicString firstName;
  publicString lastName;
  publicString email;
  public List<ResumeDto> resumes;
  public EmployeeDto(){
  }
 public EmployeeDto(Employee employee){
    id = employee.getId();
    firstName = employee.getFirstName();
    lastName = employee.getLastName();
    email = employee.getEmail();
    if (employee.getResumes() != null) {
     resumes = employee.getResumes().stream().map(ResumeDto::new).collect(Collectors.toList());
    }
  }
}

Once again I draw attention to the fact that it is so indecent to write logic in dto that all fields are designated as publicnot to use getters and setters.


5. Service


EmployeeService:


publicclassEmployeeService{
  private EmployeeDao employeeDao;
  private ResumeDao resumeDao;
  @InjectpublicEmployeeService(EmployeeDao employeeDao, ResumeDao resumeDao){
    this.employeeDao = employeeDao;
    this.resumeDao = resumeDao;
  }
  @Transactionalpublic EmployeeDto createEmployee(EmployeeCreateDto employeeCreateDto){
    Employee employee = new Employee();
    employee.setFirstName(employeeCreateDto.firstName);
    employee.setLastName(employeeCreateDto.lastName);
    employee.setEmail(employeeCreateDto.email);
    employeeDao.save(employee);
    returnnew EmployeeDto(employee);
  }
  @Transactionalpublic ResumeDto createResume(ResumeCreateDto resumeCreateDto){
    Resume resume = new Resume();
    resume.setEmployee(employeeDao.getById(resumeCreateDto.employeeId));
    resume.setPosition(resumeCreateDto.position);
    resume.setAbout(resumeCreateDto.about);
    resumeDao.save(resume);
    returnnew ResumeDto(resume);
  }
  @Transactional(readOnly = true)
  public EmployeeDto getEmployeeById(Integer id){
    returnnew EmployeeDto(employeeDao.getById(id));
  }
  @Transactional(readOnly = true)
  public ResumeDto getResumeById(Integer id){
    returnnew ResumeDto(resumeDao.getById(id));
  }
}

Those same transactions that save us from LazyInitializationException(and not only). To understand transactions in hibernate, I recommend an excellent work on Habré ( read more ... ), which helped me a lot in due time.


6. Resources


Finally, add the resources to create and receive our entities
EmployeeResource::


@Path("/")@SingletonpublicclassEmployeeResource{
  privatefinal EmployeeService employeeService;
  public EmployeeResource(EmployeeService employeeService) {
    this.employeeService = employeeService;
  }
  @GET@Produces("application/json")@Path("/employee/{id}")@ResponseBodypublic Response getEmployee(@PathParam("id") Integer id) {
    return Response.status(Response.Status.OK)
        .entity(employeeService.getEmployeeById(id))
        .build();
  }
  @POST@Produces("application/json")@Path("/employee/create")@ResponseBodypublic Response createEmployee(@RequestBody EmployeeCreateDto employeeCreateDto){
    return Response.status(Response.Status.OK)
        .entity(employeeService.createEmployee(employeeCreateDto))
        .build();
  }
  @GET@Produces("application/json")@Path("/resume/{id}")@ResponseBodypublic Response getResume(@PathParam("id") Integer id) {
    return Response.status(Response.Status.OK)
        .entity(employeeService.getResumeById(id))
        .build();
  }
  @POST@Produces("application/json")@Path("/resume/create")@ResponseBodypublic Response createResume(@RequestBody ResumeCreateDto resumeCreateDto){
    return Response.status(Response.Status.OK)
        .entity(employeeService.createResume(resumeCreateDto))
        .build();
  }
}

Produces(“application/json”)It is necessary that json and dto are correctly transformed into each other. It requires the pom.xml dependency:


<dependency><groupId>org.glassfish.jersey.media</groupId><artifactId>jersey-media-json-jackson</artifactId><version>${jersey.version}</version></dependency>

Other json converters for some reason expose an invalid mediaType.


7. Result


Run and check what we got ( mvn clean install exec:javaat the root of the project). The port on which the application is launched is specified in service.properties . Create a user and resume. I do this with curl, but you can use postman if you despise the console.


curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"firstName": "Jason", "lastName": "Statham", "email": "jasonst@t.ham"}' \
  http://localhost:9999/employee/create
curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"employeeId": 0, "position": "Voditel", "about": "Opyt raboty perevozchikom 15 let"}' \
  http://localhost:9999/resume/create
curl --header "Content-Type: application/json" --request GET http://localhost:9999/employee/0
curl --header "Content-Type: application/json" --request GET http://localhost:9999/employee/0

Everything works perfectly. Thus, we got a backend providing api. Now you can start the service with the front-end and draw the corresponding forms. This is a good foundation of the application, which you can use to start your own, configuring various components as the project progresses.


Conclusion


The main application code is contained in working condition on a githaba with instructions for launching it in a wiki tab. Screenshots that have been promised:




For a multi-million dollar project, it looks a bit damp, of course, but as an excuse, let me remind you that we worked on it in the evening, after work / study.
If the number of interested ones exceeds the number of slippers, in the future I can turn it into a series of articles, where I will tell you about the front, the backdocking and the nuances that we encountered when working with mail / grease / dock files.


PS After some time, having gone through the shock of the school, the rest of the team gathered and, after analyzing the flights, decided to make an adaptation of 2.0, taking into account all the errors. The main objective of the project is the same - to learn how to make serious applications, build a well-thought-out architecture and be in-demand specialists in the market. You can follow the work in the same repository. Pool requests are welcome. Thank you for your attention and wish us good luck!


buns


ioc video lecture from hh


Also popular now: