Как не платить 199 рублей/неделю за hh Pro, и при этом найти работу джуну без проблем и откликов — Java выручит
В современном мире поиск работы может быть сложной и утомительной задачей. Особенно это касается начинающих специалистов, которые только начинают свой путь в профессии. В условиях жёсткой конкуренции и большого количества предложений от работодателей важно не только найти подходящую вакансию, но и выделиться среди других кандидатов.

Да, примерно так и выглядит поиск работы у джуна :)
Именно поэтому я рад представить вам прикольного бота на Java, которая поможет вам в поиске работы джуну, не тратя при этом 199 рублей каждую неделю за hh Pro. Оно базируется на API самого хедхантера, поэтому всё легально, и не требует установки Google Chrome и Selenium на сервер.
Но прежде чем мы перейдём к самой статье, хотелось бы рассказать вам о том, как она была написана. Автор статьи — Junior Java-разработчик, который свою первую работу нашёл только с помощью «холодного найма» — отклики никак не помогали, а вот пассивный поиск реально помог мне устроиться в AISA, а сейчас я чуть ли не нашёл вторую работу таким же методом.
Стек технологий
Этот бот использует только Java 21, Spring Boot 3.4, и Spring Web с вырезанным и отключенным Tomcat для уменьшения объёма JAR-файла, а также для снижения потребляемого объёма ОЗУ. Итоговый JAR-файл весит 16,5 Мбайт, что очень круто для приложения на Spring так-то.
Почему Spring?
Я мог бы написать всё это на чистом Java, но Spring даёт множество преимуществ — удобный RestClient вместо стандартного API из Java 11, удобный шедуллер для запуска метода только в определённый промежуток времени, IoC и DI, удобный менеджмент конфигурациями приложения, и многое другое.
Практика
Начинается всё с класса HhApiUtils.java — там находится логика работы с HeadHunter API. Начало выглядит так:
@Value("${ru.gavrilovegor519.hh-autoupdate-resume.authToken}")
private String authToken;
@Value("${ru.gavrilovegor519.hh-autoupdate-resume.clientId}")
private String clientId;
@Value("${ru.gavrilovegor519.hh-autoupdate-resume.clientSecret}")
private String clientSecret;
Здесь мы берём параметры из application.properties — auth_token, client_id, и client_secret, которые требуются для HeadHunter API.
private final ObjectMapper objectMapper = new ObjectMapper();
private final RestClient restClient = RestClient.builder()
.baseUrl("https://api.hh.ru")
.defaultHeader("User-Agent", "hh-autoupdate-resume/1.0 (<м>)")
.build();
Здесь мы создаём объект ObjectMapper’а, а также RestClient’а. User-Agent требуется обязательно при обращении к HeadHunter API — здесь указывается имя приложения, версия, а также почта разработчика для обращений со стороны HeadHunter.
В SendTelegramNotification аналогично — принцип тот же:
@Value("${ru.gavrilovegor519.hh-autoupdate-resume.telegram.botToken}")
private String botToken;
@Value("${ru.gavrilovegor519.hh-autoupdate-resume.telegram.chatId}")
private String chatId;
private final ObjectMapper objectMapper = new ObjectMapper();
private final RestClient restClient = RestClient.builder()
.baseUrl("https://api.telegram.org")
.build();
А вот так мы обращаемся к API:
public void updateResume(String resumeId, String accessToken) {
restClient.post()
.uri("/resume/{resumeId}/publish", resumeId)
.header("Authorization", "Bearer " + accessToken)
.accept(MediaType.APPLICATION_JSON)
.exchange((request, response) -> {
if (response.getStatusCode().isSameCodeAs(HttpStatusCode.valueOf(204))) {
return objectMapper.readValue(response.getBody(), new TypeReference<>() {
});
} else {
throw new HttpClientErrorException(response.getStatusCode(), new String(response.getBody().readAllBytes()));
}
});
}
Особое внимание на способ обращения к Telegram API — для легковесности приложения я решил использовать прямое обращение по HTTPS, без использования сторонних библиотек. Это выглядит также, как и с HH API:
public void send(String message) {
restClient.get()
.uri("/{botToken}/sendMessage?chat_id={chatId}&text={message}", "bot" + botToken, chatId, message)
.accept(MediaType.APPLICATION_JSON)
.exchange((request, response) -> {
if (response.getStatusCode().isSameCodeAs(HttpStatusCode.valueOf(200))) {
return objectMapper.readValue(response.getBody(), new TypeReference<>() {
});
} else {
throw new HttpClientErrorException(response.getStatusCode(), new String(response.getBody().readAllBytes()));
}
});
}
Таким образом приложение становится легче. Очевидно, что для отправки уведомлений этого очень даже достаточно.
А вот так выглядит часть класса AutoUpdateResume.java:
@Scheduled(fixedRate = 14400000)
public void updateResume() {
if (accessToken != null && refreshToken != null) {
try {
updateResumeInternal();
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatusCode.valueOf(403)) {
updateTokens(false);
updateResumeInternal();
}
}
} else {
updateTokens(true);
updateResumeInternal();
}
}
Принцип предельно прост — если токенов нету, приложение получает начальные токены. Если есть — приложение обновляет резюме, если ошибка — пробует обновить токены с помощью refresh_token, и пробует ещё раз.
А вот так выглядит ещё один метод:
private void updateResumeInternal() {
try {
hhApiUtils.updateResume(resumeId, accessToken);
sendTelegramNotification.send("Резюме обновлено");
} catch (Exception e) {
sendTelegramNotification.send("Ошибка обновления резюме: " + e.getMessage());
throw e;
}
}
Если вкратце — он обновляет резюме как раз. Если всё нормально — приложение не выводит exception’ов. Если есть exception’ы — он их отправляет методу выше, и он, собственно, и вызывает процесс обновления резюме.
Заключение
Вот такой код у меня получился. Я хочу, чтобы этот проект помог мне и дальше находить интересную работу без лишних мучений. Я теперь понял, что пассивный поиск даже эффективнее, чем активный («горячий») поиск работы, и (самое главное!) не требует использования методов Антона Назарова.
GitHub проекта
Habrahabr.ru прочитано 8476 раз
