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

Именно поэтому я рад представить вам бота на 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;
}
}
Кратко говоря, он отвечает за обновление резюме. Если все проходит успешно, приложение не генерирует исключений. Если возникают исключения, они передаются вышестоящему методу, который инициирует процесс обновления резюме.
Заключение
Вот такой код у меня получился. Надеюсь, этот проект поможет мне и дальше находить интересные рабочие места без особых трудностей. Теперь я понимаю, что пассивный поиск даже эффективнее активного («горячего») поиска работы, и самое важное — он не требует использования методов Антона Назарова.
Не забывайте про карму (плюс/минус - не важно) - ваше мнение очень важно для меня :)