嗨,哈伯,我们想谈谈程序员学院HeadHunter 2018的一个项目。以下是我们毕业生的一篇文章,他将在其中谈论在培训中获得的经验。

大家好 今年,我从hh程序员学院毕业,在这篇文章中,我将讨论我参加的培训项目。 在学校训练期间,尤其是在项目训练期间,我错过了一个战斗应用程序示例(甚至更好的是一个指南),在其中我可以看到如何正确分离逻辑并构建可伸缩的体系结构。 我发现的所有文章对于初学者来说都很难理解,因为 要么IoC一直在积极地使用它们,而没有对如何添加新组件或修改旧组件的全面解释,或者它们是过时的,并且包含大量的xml配置和jsp的前端。 我试图在训练之前专注于自己的水平,即 几乎需要注意的地方几乎为零,因此本文对学校的未来学生以及决定开始用Java编写的自学成才的爱好者都是有用的。
给定(问题陈述)
团队-5人。 期限为3个月,每期末都有一个演示。 目标是创建一个应用程序,该应用程序可帮助HR在试用期内陪同员工,使所有流程自动化。 在入口处,我们被告知如何安排试用期(IS):一旦得知有新员工离职,HR便开始踢未来的领导者为IP设置任务,这需要在第一个工作日之前完成。 在员工上班的那一天,HR举行欢迎会议,讨论公司的基础架构,并移交IP任务。 在1.5个月和3个月之后,举行了一次人力资源,领导和员工的中期和最终会议,讨论了通过的成功之处,并草拟了结果表。 如果成功,在最后一次会议之后,将向员工发放一份印刷的新手调查表(以“享受IP的乐趣”的方式提问),并在jira中进行人力资源工作以填写VHI的员工。
设计方案
我们决定为每个员工制作一个个人页面,在该页面上显示一般信息(姓名,部门,经理等),一个用于评论和更改历史的字段,附加文件(IP上的任务,调查表)以及反映员工的工作流程IP的通过级别。 决定将工作流程分为8个阶段,即:
- 第1阶段-添加员工:在HR系统中注册新员工后,该工作立即完成。 同时,将三个日历发送给人力资源部进行一次井,一次中间会议和最后一次会议。
- 第二阶段-IP任务的协调:将表格发送给负责IP任务的负责人,填写后,HR将收到该表单。 接下来,HR将其打印,签名并在界面中标记该阶段的完成。
- 第三阶段-欢迎会议:HR举行会议并按下“阶段已完成”按钮。
- 第四阶段-临时会议:类似于第三阶段
- 第五阶段-临时会议的结果:人力资源部门在员工页面上填写结果,然后单击“下一步”。
- 第六阶段-最终会议:类似于第三阶段
- 第七阶段-最终会议的结果:类似于第五阶段
- 第八阶段-完成IP:在成功完成IP的情况下,将通过电子邮件向员工发送问卷调查表的链接,并在jira中自动创建自愿医疗保险注册任务(该任务在我们之前启动)。
所有阶段都有时间,之后该阶段将被视为已过期,并以红色突出显示,并且通知会到达邮件中。 结束时间必须是可编辑的,例如,在临时会议是公众假期或出于某种原因需要重新安排会议的情况下。
不幸的是,在一张纸/板上绘制的原型尚未保存,但最终会有完成的应用程序的屏幕截图。
运作方式
学校的目标之一是为学生做好在大型项目中工作的准备,因此发布任务的过程适合我们。
在完成该任务后,我们将其提交给团队中的另一名学生进行review_1,以纠正明显的错误/交流经验。 然后是review_2-任务由两名指导者检查,以确保我们不会与reviewer_1一起释放govnokod。 本来应该进行进一步的测试,但是考虑到学校项目的规模,这个阶段不是很合适。 因此,经过审查之后,我们认为任务已准备好发布。
现在介绍一下部署。 该应用程序应始终可从任何计算机在网络上使用。 为此,我们购买了便宜的虚拟机(每月100卢布),但据我后来了解到,一切都可以在AWS docker中免费以时尚的方式安排。 为了进行持续集成,我们选择了Travis。 如果没有人知道(我个人上学前从未听说过进行持续集成),这真是太棒了,您的github将对其进行监视,并在出现新提交(如何配置)时将其收集在jar中的代码,将其发送到服务器并自动重新启动应用程序。 在项目根源的Travis Jam中描述了如何构建它,它与bash非常相似,因此我认为不需要评论。 我们还购买了域名www.adaptation.host,以免在演示的地址栏中注册难看的IP地址。 我们还配置了postfix(用于发送邮件),apache(不是nginx,因为apache已开箱)和jira(试用)服务器。 前端和后端是由两个单独的服务组成的,这些服务将通过http进行通信(#2k18,#microservices)。 本文的“在HeadHunter程序员学院”的这一部分顺利结束,我们继续进行Java Rest服务。
后端
0.简介
我们使用了以下技术:
- JDK 1.8;
- Maven 3.5.2;
- Postgres 9.6;
- 休眠5.2.10;
- 码头9.4.8;
- 泽西岛2.27。
作为框架,我们从hh提取了NaB 3.5.0。 首先,它在HeadHunter中使用,其次,它包含开箱即用的码头,运动衫,冬眠,嵌入式postgres,这些都写在github上。 我将为初学者简要说明一下:jetty是一种网络服务器,可以识别客户端并为每个客户端组织会话; jersey-帮助方便地创建RESTful服务的框架; 休眠-ORM以简化数据库的工作; Maven是一个Java项目收集器。
我将显示一个简单的示例,说明如何使用它。 我创建了一个小型测试存储库 ,在其中添加了两个实体:一个用户和一个简历,以及用于通过OneToMany / ManyToOne链接创建和接收它们的资源。 首先,只需克隆存储库并在项目的根目录中运行mvn clean install exec:java。 在对代码进行评论之前,我将向您介绍我们的服务结构。 看起来像这样:

主目录:
- 服务-应用程序中的主目录,所有业务逻辑都存储在这里。 在其他地方,不应使用没有充分理由的数据。
- 资源-URL处理程序,服务和前端之间的一层。 此处允许输入数据的验证和输出数据的转换,但不允许业务逻辑。
- Dao(数据访问对象)-数据库和服务之间的一层。 Tao应该只包含基本的基本操作:添加,计数,更新,删除一个/全部。
- 实体-ORM与数据库交换的对象。 通常,它们直接与表相对应,并且应包含所有字段作为数据库中具有相应类型的实体。
- Dto(数据传输对象)-实体的类似物,仅用于资源(前端),有助于从我们要发送/接收的数据中形成json。
1.基础
以一种很好的方式,您应该像在主应用程序中一样在附近使用已安装的postgres,但是我希望测试用例简单并且可以用一个命令运行,所以我采用了内置的HSQLDB。 通过将数据源添加到ProdConfig来完成将数据库连接到我们的基础架构的操作(还请记住告诉hibernate您正在使用哪个数据库):
@Bean(destroyMethod = "shutdown") DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("db/sql/create-db.sql") .build(); }
我在create-db.sql文件中创建了表创建脚本。 您可以添加其他脚本来初始化数据库。 在我们轻量级的in_memory示例中,我们完全不需要脚本。 如果您在hibernate.properties设置中指定hibernate.hbm2ddl.auto=create
,则当应用程序启动时,hibernate本身将按实体创建表。 但是,如果您需要数据库中实体没有的东西,那么就不能没有文件。 就个人而言,我习惯于共享数据库和应用程序,因此我通常不信任休眠来执行此类操作。
db/sql/create-db.sql
:
CREATE TABLE employee ( id INTEGER IDENTITY PRIMARY KEY, first_name VARCHAR(256) NOT NULL, last_name VARCHAR(256) NOT NULL, email VARCHAR(128) NOT NULL ); CREATE TABLE resume ( id INTEGER IDENTITY PRIMARY KEY, employee_id INTEGER NOT NULL, position VARCHAR(128) NOT NULL, about VARCHAR(256) NOT NULL, FOREIGN KEY (employee_id) REFERENCES employee(id) );
2.实体
entities/employee
:
@Entity @Table(name = "employee") public class Employee { @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;
entities/resume
:
@Entity @Table(name = "resume") public class Resume { @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;
实体之间不会通过class字段相互引用,而是通过整个父/子对象相互引用。 因此,当我们尝试从Employee数据库中获取要为其绘制简历的递归时,为了...为了防止这种情况的发生,我们分别指出了@OneToMany(mappedBy = "employee")
和@ManyToOne(fetch = FetchType.LAZY)
。 从数据库执行写/读事务时,将在服务中将它们考虑在内。 设置FetchType.LAZY
是可选的,但是使用惰性通信会使事务更容易。 因此,如果在交易中我们从数据库中获取简历,并且没有联系其所有者,那么将不会加载员工实体。 您可以自己验证:删除FetchType.LAZY
并在调试中看到它与恢复一起从服务返回。 但是您应该小心-如果我们没有在事务中加载employee,那么访问事务外部的employee字段可能会导致LazyInitializationException
。
3.道
在我们的例子中,EmployeeDao和ResumeDao几乎相同,因此在这里我只给出其中之一
EmployeeDao
:
public class EmployeeDao { private final SessionFactory sessionFactory; @Inject public EmployeeDao(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public void save(Employee employee) { sessionFactory.getCurrentSession().save(employee); } public Employee getById(Integer id) { return sessionFactory.getCurrentSession().get(Employee.class, id); } }
@Inject
意味着在我们的dao的构造函数中,使用了依赖注入。 在我的前世中,一位物理学家打包文件,根据数字结果构建图形,并且至少在Java指南中弄清楚了OOP,这种构造似乎有些疯狂。 在学校里,也许这个话题是最不明显的,恕我直言。 幸运的是,Internet上有很多有关DI的资料。 如果您懒于阅读,则可以在第一个月遵循以下规则:在我们的context-config中注册新的资源/服务/ Tao,将实体添加到映射中 。 如果需要在其他服务中使用某些服务/ tao,则需要使用注解inject将它们添加到构造函数中,如上所示,并且spring会为您初始化所有内容。 但是随后您仍然必须处理DI。
4. DTO
Dto和dao一样,对于员工和简历来说几乎相同。 在这里,我们仅考虑employeeDto。 我们将需要两个类: EmployeeCreateDto
,在创建雇员时是必需的; 收据上使用的EmployeeDto
(包含其他字段id
和resumes
)。 添加了id
字段,以便将来在外部发出请求时,我们可以与员工合作,而无需通过email
对实体进行初步搜索。 resumes
字段用于在一个请求中接收雇员及其所有简历。 可以使用一个dto进行所有操作的管理,但是对于特定雇员的所有简历的列表,我们必须创建一个额外的资源,例如getResumesByEmployeeEmail,使用自定义数据库查询来污染代码,并消除ORM提供的所有便利。
EmployeeCreateDto
:
public class EmployeeCreateDto { public String firstName; public String lastName; public String email; }
EmployeeDto
:
public class EmployeeDto { public Integer id; public String firstName; public String lastName; public String 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()); } } }
我再次提请注意以下事实:在dto中编写逻辑是如此不雅致,以至于所有字段都被指定为public
,从而不使用getter和setter。
5.服务
EmployeeService
:
public class EmployeeService { private EmployeeDao employeeDao; private ResumeDao resumeDao; @Inject public EmployeeService(EmployeeDao employeeDao, ResumeDao resumeDao) { this.employeeDao = employeeDao; this.resumeDao = resumeDao; } @Transactional public EmployeeDto createEmployee(EmployeeCreateDto employeeCreateDto) { Employee employee = new Employee(); employee.setFirstName(employeeCreateDto.firstName); employee.setLastName(employeeCreateDto.lastName); employee.setEmail(employeeCreateDto.email); employeeDao.save(employee); return new EmployeeDto(employee); } @Transactional public 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); return new ResumeDto(resume); } @Transactional(readOnly = true) public EmployeeDto getEmployeeById(Integer id) { return new EmployeeDto(employeeDao.getById(id)); } @Transactional(readOnly = true) public ResumeDto getResumeById(Integer id) { return new ResumeDto(resumeDao.getById(id)); } }
那些保护我们免受LazyInitializationException
(并且不仅是)的交易。 为了了解休眠状态下的事务,我建议在Hub上进行出色的工作( 了解更多信息 ),这在适当的时候对我有很大帮助。
6.资源
最后,添加资源以创建并获取我们的实体:
EmployeeResource
:
@Path("/") @Singleton public class EmployeeResource { private final EmployeeService employeeService; public EmployeeResource(EmployeeService employeeService) { this.employeeService = employeeService; } @GET @Produces("application/json") @Path("/employee/{id}") @ResponseBody public Response getEmployee(@PathParam("id") Integer id) { return Response.status(Response.Status.OK) .entity(employeeService.getEmployeeById(id)) .build(); } @POST @Produces("application/json") @Path("/employee/create") @ResponseBody public Response createEmployee(@RequestBody EmployeeCreateDto employeeCreateDto){ return Response.status(Response.Status.OK) .entity(employeeService.createEmployee(employeeCreateDto)) .build(); } @GET @Produces("application/json") @Path("/resume/{id}") @ResponseBody public Response getResume(@PathParam("id") Integer id) { return Response.status(Response.Status.OK) .entity(employeeService.getResumeById(id)) .build(); } @POST @Produces("application/json") @Path("/resume/create") @ResponseBody public Response createResume(@RequestBody ResumeCreateDto resumeCreateDto){ return Response.status(Response.Status.OK) .entity(employeeService.createResume(resumeCreateDto)) .build(); } }
Produces(“application/json”)
,以便json和dto正确地相互转换。 它需要一个pom.xml依赖项:
<dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>${jersey.version}</version> </dependency>
由于某些原因,其他json转换器会公开无效的MediaType。
7.结果
运行并检查我们拥有什么( mvn clean install exec:java
项目根目录中的mvn clean install exec:java
)。 在service.properties中指定了运行应用程序的端口。 创建一个用户并继续。 我用curl做到这一点,但是如果您鄙视控制台,则可以使用邮递员。
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
一切正常。 因此,我们有一个提供api的后端。 现在,您可以从前端启动该服务并绘制相应的表格。 这是一个应用程序的良好基础,您可以通过在项目开发过程中配置各种组件来启动自己的应用程序。
结论
主要应用程序代码在github上保持正常运行,并在Wiki选项卡中提供了启动说明。


当然,对于一个价值数百万美元的项目而言,它看起来有点潮湿,但是作为借口,我想提醒您,我们在下班/学习后的晚上进行了这项工作。
如果感兴趣的人数超过了拖鞋的数量,将来我可以将其变成一系列文章,在这些文章中,我将讨论在处理邮件/ fat / dock文件时遇到的前端,泊坞窗化和细微差别。
PS在经历了学校的冲击后,经过一段时间后,团队的其余成员聚在一起,在分析了飞行情况之后,考虑到所有错误,决定采用2.0版。 该项目的主要目标是相同的-学习如何进行严肃的应用,构建周到的体系结构以及市场专家的需求。 您可以在同一存储库中关注工作。 欢迎泳池要求。 感谢您的关注,并祝我们好运!
头
ioc特设视频讲座