Русский
Русский
English
Статистика
Реклама

Обработка ошибок

Перевод Лучшая практика обработки ошибок в современном JavaScript

07.10.2020 16:09:34 | Автор: admin

Когда вы пишете код, важно учитывать ситуации, приводящие к ошибкам. Обработка ошибок это неотъемлемая часть работы над веб-приложением. Мы посмотрим на некоторые рекомендации по обработке ошибок в JavaScript. Чтобы не тратить ваше время зря, сразу поясняем, что описанное в статье может быть не в новинку многопытным кодерам. Если вы себя таким считаете смело пропускайте этот материал, всех остальных приглашаем под кат.



Расширяем класс Error


Часто бывает полезно предоставить детальное описание ошибки внутри обработчика. И под этим я подразумеваю не только четкое сообщения об ошибке. Я имею в виду расширение класса Error. Расширив класс Error, вы можете настроить полезные при отладке свойства name и message, а также написать пользовательские геттеры, сеттеры и другие методы:

class BadParametersError extends Error {  name = 'BadParametersError'  constructor(message) {    super(message)  }  get recommendation() {    return this._recommendation  }  set recommendation(recommendation) {    this._recommendation = recommendation  }}

Такой подход может сгладить отладку, когда несколько сценариев в вашем коде бросают ту же ошибку по одним и тем же причинам и нуждаются в дополнительном контексте. Немного сложно вернуться назад и точно выяснить причину без утилиты (конечно, в зависимости от ошибки).

Посмотрим на ситуацию, когда расширение Error приносит пользу. Допустим, у вас есть функция, принимающая список функций решателя. Она принимает аргумент, проходит по списку решателей и передает аргумент в каждую функцию. Если функция возвращает какой-то результат, проход останавливается и этот результат возвращается функцией:

// Takes a list of resolvers, composes them and returns a func that calls// each resolvers on the provided args.function composeResolvers(...resolvers) {  return (args) => {    let result    for (let index = 0; index < resolvers.length; index++) {      const resolve = resolvers[index]      result = resolve(args)      if (result) {        break // Abort the loop since we now found a value      }    }    return result  }}

Представьте, что вы пишете страницу, где пользователю предлагается ввести год рождения, чтобы определить его в какую-то группу:

import composeResolvers from '../composeResolvers'const resolvers = []const someResolverFn = (userInput) => {  if (userInput > 2002) {    return 'NewKidsOnTheBlock'  }  return 'OldEnoughToVote'}// Pretending our code is only stable/supported by certain browsersif (/chrome/i.test(navigator.userAgent)) {  resolvers.push(someResolverFn)}const resolve = composeResolvers(...resolvers)window.addEventListener('load', () => {  const userInput = window.prompt('What year was your computer created?')  const result = resolve(userInput)  window.alert(`We recommend that you register for the group: ${result}`)})

Когда пользователь нажимает OK, его возраст присваивается userInput и передается в качестве аргумента функции из composeResolvers:

import composeResolvers from '../composeResolvers'const resolvers = []const someResolverFn = (userInput) => {  if (userInput > 2002) {    return 'NewKidsOnTheBlock'  }  return 'OldEnoughToVote'}// Pretending our code is only stable/supported by certain browsersif (/chrome/i.test(navigator.userAgent)) {  resolvers.push(someResolverFn)}const resolve = composeResolvers(...resolvers)window.addEventListener('load', () => {  const userInput = window.prompt('What year was your computer created?')  const result = resolve(userInput)  window.alert(`We recommend that you register for the group: ${result}`)})



По окончании работы запускается window.alert, чтобы показать пользователю его группу:



Код работает нормально. Но что, если пользователь смотрит страницу не в Chrome? Тогда строка resolvers.push(someResolverFn) не работает. Ниже мы видим неприятный результат:



Мы можем предупредить необработанные ошибки, бросив обычную Error, или можем использовать более подходящую BadParametersError:

// Takes a list of resolvers, composes them and returns a func that calls// each resolvers on the provided args.function composeResolvers(...resolvers) {  if (!resolvers.length) {    const err = new BadParametersError(      'Need at least one function to compose resolvers',    )    err.recommendation =      'Provide a function that takes one argument and returns a value'    throw err  }  return (args) => {    let result    for (let index = 0; index < resolvers.length; index++) {      const resolve = resolvers[index]      result = resolve(args)      if (result) {        break // Abort the loop since we now found a value      }    }    return result  }}

Так у ошибки гораздо меньше шансов попасть к пользователю. Сообщение заставляет разработчика исправить ситуацию:



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

const resolve = composeResolvers(...resolvers)window.addEventListener('load', () => {  const userInput = window.prompt('What year was your computer brought to you?')  let result  try {    result = resolve(userInput)  } catch (error) {    if (error instanceof BadParametersError) {      console.error(        `[Error] ${error.message}. Here's a recommendation: ${error.recommendation}`,      )      console.log(error.recommendation)    } else {      // Do some fallback logic      return window.alert(        'We are sorry, there was a technical problem. Please come back later',      )    }  }  window.alert(`We recommend that you register for the group: ${result}`)})

Применение TypeError


Мы часто работаем с Error, но когда есть более подходящая встроенная ошибка, полезно не пренебрегать ей:

async function fetchDogs(id) {  let result  if (typeof id === 'string') {    result = await api.fetchDogs(id)  } else if (typeof id === 'array') {    result = await Promise.all(id.map((str) => api.fetchDogs(id)))  } else {    throw new TypeError(      'callSomeApi only accepts a string or an array of strings',    )  }  return result}const params = { id: 'doggie123' }let dogsfetchDogs(params)  .then((dogs) => {    dogs = dogs  })  .catch((err) => {    if (err instanceof TypeError) {      dogs = Promise.resolve(fetchDogs(params.id))    } else {      throw err    }  })


Тестирование


Благодаря наследованию Error тестирование становится надежнее. Такие ошибки можно использовать при написании ассертов:

import { expect } from 'chai'import chaiAsPromised from 'chai-as-promised'import fetchCats from '../fetchCats'chai.use(chaiAsPromised)it('should only take in arrays', () => {  expect(fetchCats('abc123')).to.eventually.rejectWith(TypeError)})


Важно не перестараться


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

class AbortExecuteError extends Error {  name = 'AbortExecuteError'  constructor(message) {    super(message)  }}class BadParameters extends Error {  name = 'BadParameters'  constructor(message) {    super(message)  }}class TimedOutError extends Error {  name = 'TimedOutError'  constructor(message) {    super(message)  }}class ArrayTooLongError extends Error {  name = 'ArrayTooLongError'  constructor(message) {    super(message)  }}class UsernameError extends Error {  name = 'UsernameError'  constructor(message) {    super(message)  }}

Тогда вы переосмысливаете подход и стараетесь понять, действительно ли вам нужна такая обработка ошибок. В большинстве случаев достаточно четко сообщить об ошибке. Ассерт приносит максимальную выгоду только в том случае, когда необходимо добавить контекст. Например, токен, чтобы повторить запрос по тайм-ауту:

class TimedOutError extends Error {  name = 'TimedOutError'  retried = 0  constructor(message) {    super(message)  }  set retry(callback) {    this._retry = callback  }  retry(...args) {    this.retried++    return this._retry(...args)  }}class ConnectToRoomTimedOutError extends TimedOutError {  name = 'ConnectToRoomTimedOutError'  constructor(message) {    super(message)  }  get token() {    return this._token  }  set token(token) {    this._token = token  }}let timeoutRefasync function connect(token) {  if (timeoutRef) clearTimeout(timeoutRef)    timeoutRef = setTimeout(() => {      const err = new ConnectToRoomTimedOutError(        'Did not receive a response from the server',      )      err.retry = connect      err.token = token      throw err    }, 10000)    const room = await api.join(token)    clearTimeout(timeoutRef)    return room  }}const joinRoom = () => getToken().then((token) => connect(token))async function start() {  try {    let room = await joinRoom()    return room  } catch (err) {    if (err instanceof ConnectToRoomTimedOutError) {      try {        // Lets retry one more time        room = await err.retry(err.token)        return room      } catch (innerErr) {        throw innerError      }    }    throw err  }}start()  .then((room) => {    console.log(`Received room, oh yea!`, room)  })  .catch(console.error)

Помните, что обработка ошибок экономит ваши деньги и время.

image

Получить востребованную профессию с нуля или Level Up по навыкам и зарплате, можно, пройдя онлайн-курсы SkillFactory:



Подробнее..

NEST.JS. Работа с ошибками. Мысли и рецепты

14.03.2021 10:05:42 | Автор: admin

Холивар...

Одни умные люди говорят: "эксепшены - это зло, используйте монады!", другие кричат: "коды ошибок ещё никто не отменял!", а третьи включают механизм исключений в языки программирования.. Однако, у большинства статей, который попадались мне на глаза, есть (при всём уважении к авторам!) два проблемных момента:

  • Некоторая... академичность. Разобрано много и интересно, но заканчивается всё стандартным: "ваш выбор зависит от вашей ситуации".

  • Абсолютно отсутствуют упоминания о бюджете. Никто же не будет спорить, что теоретически мерседес лучше, чем восьмёрка по всем показателям кроме.. цены.

Задача этого поста - поделиться выработанным практическим рецептом. В конкретном фреймворке и с конкретными границами применимости. Без претензий на уникальность, универсальность и, тем более, академическую "правильность".


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

Стартовые условия.

Выделим основные: язык, фреймворк, тип приложения. Раскроем кратко каждый пункт:

ЯЗК

Платформа и ЯП, очень сильно влияют на выбор подходов по работе с ошибками.

К примеру, в go не стоит вопрос, использовать ли исключения - там их нет. В функциональных языках, в частности в F#, было бы очень странно не использовать монады или discriminated union'ы (возврат одного из нескольких возможных типов значений), т. к. это там это реализовано очень удобным и естественным образом. В C#, монады тоже можно сделать, но получается намного больше букв. А это не всем нравится, мне например - не очень. Правда, последнее время всё чаще упоминается библиотека https://www.nuget.org/packages/OneOf/, которая фактически добавляет в язык discriminated union'ы.

А к чему нас подталкивает javascript/typescript?... К анархии! Можно много за что ругать JS и вполне по делу, но точно не за отсутствие гибкости.

Скорее уж за сверхгибкость )). В общем, мы вольны делать так, как нам хочется. Но тут есть, небольшая проблема - когда у вас в команде 10 человек и каждый делает как ему хочется.. получается не очень. Даже если каждый подход в отдельности - неплох.

ФРЕЙМВОРК

С nestjs уже интереснее. Выброс исключений из прикладного кода предлагается нам в документации как основной механизм возврата неуспешных ответов. То есть, если взять обычное http приложение, то чтобы клиенту вернулся статус 404 нам надо бросить NotFoundException..

На самом деле, довольно спорная концепция. И это можно обойти, причём разными способами. Убеждённые сторонники монад вполне могут делать что-то такое:

@Controller()class SomeController {  @Post()  do (): Either<SomeResult, SomeError> {    ...  }}

Для этого, правда придётся написать кое-какой обвязочный код, но можно. Мы не стали.

Важно также, что Фреймворк делает практически всё для того, чтобы нам не приходилось заботиться об устойчивости процесса приложения . Nest сам выстраивает для нас "конвейер" обработки запроса и оборачивает всё это в удобный глобальный "try/catch", который ловит всё.

Правда иногда случаются казусы

Например в одной из старых версий nest'а мы столкнулись с тем, что ошибка, вылетевшая из функции переданной в декоратор @Transform() (из пакета class-transformer) почему-то клала приложение насмерть. В версии 7.5.5 это не воспроизводится, но от подобных вещей, конечно никто не застрахован.

ТИП ПРИЛОЖЕНИЯ

Самое важное. Мы не пишем софт для спутников. Там вряд ли можно было бы себе позволить что-то в стиле "сервис временно недоступен, попробуйте позже". Для нас же - это вполне ожидаемая ситуация. Нежелательная, конечно, но и не фатальная.

Мы пишем веб-сервисы. Есть http-сервисы, есть rpc (на redis и RabbitMQ, смотрим в сторону gRPC), гибридные тоже есть. В любом случае, мы стараемся внутреннюю логику приложения абстрагировать от транспорта, чтобы в любой момент можно было добавить новый.

Мы фокусируемся на том, что у нас есть запрос, есть его обработчик и есть ответ (который иногда void). И мы допускаем, что обработка запроса может по каким-то причинам оказаться неудачной. В этом случае, либо запрос будет повторён (успешно), либо будет зафиксирован и затем исправлен баг.

При таком подходе, важно, чтобы ошибки не могли привести данные в неконсистентное состояние. Помогают нам в этом две вещи:

  • Транзакционность. То есть, либо получилось всё, либо не получилось ничего.

  • Идемпотентность. Повторное выполнение одной и той же команды не ломает и не меняет состояние системы.

Транзакции (особенно распределённые) и идемпотентность выходят за рамки данной статьи. Но во многом, эти вещи являются основой надёжности.

Ближе к делу.

Наши принципы обработки ошибок базируются на следующих соглашениях:

КОНФИГУРАЦИЯ ПРИЛОЖЕНИЯ.

Конфигурация всегда считывается и валидируется только при старте приложения. Соответственно, об отсутствии каких-то переменных окружения или ещё чего-то, мы узнаём сразу, а не когда-нибудь потом. Достигается это вполне тривиально. Каждый модуль приложения, которому требуется конфигурация включает в себя класс примерно такого вида:

@Injectable()export class SomeModuleConfig {  public readonly someUrl: URL;public readonly someFile: string;public readonly someArrayOfNumbers: number[];  constructor (source: ConfigurationSource) {    // Бросит ConfigurationException если не удастся распарсить Url. Можно    // также проверять его доступность, например, при помощи пакета is-reachable    this.someUrl = source.getUrl('env.SOME_URL');// Бросит ConfigurationException если файл не существует или на него нет прав.this.someFile = source.getFile('env.SOME_FILE_PATH');// Бросит ConfigurationException если там не перечисленные через запятую числаthis.someArrayOfNumbers = source.getNumbers('env.NUMBERS')  }}

Значения из конфигурации считываются только в конструкторах подобных классов и валидируются максимально строго.

Подход к валидации

Мы написали свои валидаторы. Их преимущетсво в том, что мы не только валидируем данные, но в ряде случаев, можем сделать дополнительные проверки (доступность файла или удалённого ресурса, как в примере выше, например).

Однако, вполне можно использовать joi или json-схемы (может ещё есть варианты) - кому что больше нравится.

Неизменным должно быть одно - всё валидируется на старте.

УРОВНИ АБСТРАКЦИИ.

Мы максимально чётко разделяем бизнес-код и инфраструктурный код. И всё инфраструктурное выносим в библиотеки. Более менее очевидно, но всё же приведу пример:

// Задача: скачать файл по ссылке.const response = await axios.get(url, { responseType: 'stream' });const { contentType, filename } = this.parseHeaders(response);const file = createWriteStream(path);response.data.pipe(file);file.on('error', reject);file.on('finish', () => resolve({ contentType, filename, path }));

Такому коду не место не только в бизнес-логике, но вообще в приложении. В нём нет ничего уникального, привязывающего его к какому-то контексту. Ему место в библиотеке, скажем в классе NetworkFile. Вызывающий же код может выглядеть примерно так:

const file: NetworkFile = await NetworkFile.download('https://download.me/please', {  saveAs: 'path/to/directory'});

Фактически, мы заворачиваем в подобные переиспользуемые "смысловые" абстракции почти все нативные нодовские вызовы и вызовы сторонних библиотек. Стратегия обработки ошибок в этих обёртках: "поймать -> завернуть -> бросить". Пример простейшей реализации такого класса:

export class NetworkFile {private constructor (  public readonly filename: string,    public readonly path: string,    public readonly contentType: string,    public readonly url: string  ) {}    // В примере выше у нас метод download принимает вторым аргументов объект опций  // Таким образом мы можем кастомизировать наш класс: он может записывать файл на диск  // или не записывать, например.  // Но тут для примера - самая простая реализация.  public static async download (url: string, path: string): Promise<NetworkFile> {    return new Promise<NetworkFile>(async (resolve, reject) => {      try {      const response = await axios.get(url, { responseType: 'stream' });        const { contentType, filename } = this.parseHeaders(response);        const file = createWriteStream(path);        response.data.pipe(file);// Здесь мы отловим и завернём все ошибки связанную с записью данных в файл.        file.on('error', reject(new DownloadException(url, error));        file.on('finish', () => {        resolve(new NetworkFile(filename, path, contentType, url));        })    } catch (error) {        // А здесь, отловим и завернём ошибки связанные с открытием потока или скачиванием        // файла по сети.        reject(new DownloadException(url, error))      }    });  }private static parseHeaders (    response: AxiosResponse  ): { contentType: string, filename: string } {    const contentType = response.headers['content-type'];    const contentDisposition = response.headers['content-disposition'];    const filename = contentDisposition// parse - сторонний пакет content-disposition      ? parse(contentDisposition)?.parameters?.filename as string      : null;    if (typeof filename !== 'string') {      // Создавать здесь специальный тип ошибки нет смысла, т. к. на уровень выше      // она завернётся в DownloadException.      throw new Error(`Couldn't parse filename from header: ${contentDisposition}`);    }    return { contentType, filename };  }}
Promise constructor anti-pattern

Считается не круто использовать new Promise() вообще, и async-коллбэк внутри в частности. Вот и вот - релевантные посты на stackoverflow по этому поводу.

Но мне кажется, что данный код - одно из встречающихся исключений. По крайней мере, мне не известно другого способа "промисифицировать" работу с потоками.

Уследить за потоком управления в таком маленьком классе (на самом деле, его боевая версия лишь немногим больше) - не проблема. А в итоге, вызывающий код работает только с одним типом исключений: DownloadException, внутрь которого завёрнута причина, по которой файл скачать не удалось. И причина носит исключительно информативный характер и не влияет на дальнейшую работу приложения, т. к.:

В БИЗНЕС-КОДЕ НИГДЕ НЕ НАДО ПИСАТЬ TRY / CATCH.

Серьёзно, о таких вещах, как закрытие дескрипторов и коннектов не должна заботиться бизнес-логика! Если вам прям очень надо написать try / catch в коде приложения, подумайте.. либо вы пишете то, что должно быть вынесено в библиотеку. Либо.. вам придётся объяснить товарищам по команде, почему именно здесь необходимо нарушить правило (хоть и редко, но такое всё же бывает).

Так почему не надо в сервисе ничего ловить? Для начала:

ЧТО М СЧИТАЕМ ИСКЛЮЧИТЕЛЬНОЙ СИТУАЦИЕЙ?

Откровенно говоря, в этом месте мы сломали немало копий. В конце концов, копья кончились, и мы пришли к концепции холивар-agnostic. Зачем нам отвечать на этот провокационный вопрос? В нём очень легко утонуть, причём мы будем не первыми утопленниками )

Наша концепция проста - при возникновении любой ошибки мы, без споров о её исключительности, завершаем работу обработчика. Никакого геройства - никто не пытается спасать положение!

Не смогли считать файл - до свиданья. Не смогли распарсить ответ от стороннего API - до свидания. В базе duplicate key - до свидания. Не можем найти указанную сущность - до свидания. Максимально просто. И механизм throw, даёт нам удобную возможность осуществить этот быстрый выход без написания дополнительного кода.

В основном исключения ругают за две вещи:

  • Плохой перформанс. Нас это не очень волнует, т. к. мы не highload. Если он нас всё же в какой-то момент настигнет, мы, пересмотрим подходы там, где это будет реально критично. Сделаем бенчмарки... Хотя, готов поспорить, оверхед на исключения будет не главной нашей проблемой.

  • Запутывание потока управления программы. Это как оператор goto который уже давно не применяется в высокоуровневых программах. Вот только в нашем случае, goto бывает только в одно место - к выходу. А ранний return из функции - отнють не считается анти-паттерном. Напротив - это очень широко используемый способ уменьшить вложенность кода.

ВИД ОШИБОК

Говорят, что надо обрабатывать исключения там, где мы знаем, что с ними делать. В нашем слое бизнес-логики ответ будет всегда один и тот же: откатить транзакцию, если она есть (автоматически), залогировать всё что можно залогировать и вернуть клиенту ошибку. Вопрос в том, какую?

Мы используем 5 типов рантайм-исключений (про конфигурационные уже говорил выше):

abstract class AuthenticationException extends Exception {  public readonly type = 'authentication';}abstract class NotAllowedException extends Exception {public readonly type = 'authorization';}abstract class NotFoundException extends Exception {  public readonly type = 'not_found';}abstract class ClientException extends Exception {  public readonly type = 'client';}abstract class ServerException extends Exception {  public readonly type = 'server';}

Эти классы семантически соответствуют HTTP-кодам 401, 403, 404, 400 и 500. Конечно, это не вся палитра из спецификации, но нам хватает. Благодаря соглашению, что всё, что вылетает из любого места приложения должно быть унаследовано от указанных типов, их легко автоматически замапить на HTTP ответы.

А если не HTTP? Тут надо смотреть конкретный транспорт. К примеру один из используемых у нас вариантов подразумевает получения сообщения из очереди RabbitMQ и отправку ответного сообщения в конце. Для сериализации ответа мы используем.. что-то типа either:

interface Result<T> {data?: T;  error?: Exception}

Таким образом, есть всё необходимое, чтобы клиентский код мог интерпретировать ошибку принятым у нас способом.

Базовый класс Exception выглядит примерно так:

export abstract class Exception {  abstract type: string;  constructor (    public readonly code: number,    public readonly message: string,    public readonly inner?: any  ) {}toString (): string {    // Здесь логика сериализации, работа со стек-трейсами, вложенными ошибками и проч...  }}
Коды ошибок

Коды ошибок - это то, что мы начали внедрять буквально месяц назад, поэтому пока не могу уверенно говорить об их практической пользе. Мы надеемся на то, что они послужат двум целям:

  • Бывает такое, что клиентское приложение должно предпринять различные действия в зависимости от пришедшей от сервера ошибки. С кодами мы можем решить это не добавляя новых http статусов и без, прости Господи, парсинга сообщений.

  • Мы сможем автоматически сформировать и поддерживать индексированный справочник ошибок, которым потом будет пользоваться наша служба технической поддержки. Там будет более подробное описание ошибок, с указанием возможных способов их исправления, паролями и явками - куда бежать.

Насколько это всё нужно и полезно - жизнь покажет

Поле inner - это внутренняя ошибка, которая может быть "завёрнута" в исключение (см. пример с NetworkFile).

Реализации абстрактных дочерних классов содержат в себе только значение поля type. Это удобно для сериализации, но можно обойтись и без него. В буквальном смысле - тип ради типа.

ПРИМЕР ИСПОЛЬЗОВАНИЯ

Опустим AuthenticationException - он используется у нас только в модуле контроля доступа. Разберём более типовые примеры и начнём ошибок валидации:

import { ValidatorError } from 'class-validator';// ....export interface RequestValidationError {  // Массив - потому что ошибка может относиться к нескольким полям.  properties: string[];  errors: { [key: string]: string };nested?: RequestValidationError[]}// Небольшая трансформация стандартной ошибки class-validator'а в более удобный// "наш" формат.const mapError = (error: ValidationError): RequestValidationError => ({  properties: [error.property],  errors: error.constraints,  nested: error.children.map(mapError)});// Сами цифры не имеют значения.export const VALIDATION_ERROR_CODE = 4001;export class ValidationException extends ClientException {  constructor (errors: ValidationError[]) {    const projections: ValErrorProjection[] = ;    super(      VALIDATION_ERROR_CODE,      'Validation failed!',      errors.map(mapError)    );  }}

Иногда валидация производится в пайпе, иногда где-то в обработчике, но мы всегда хотим, чтобы на выходе был единообразный, удобный для нас формат. Заставить стандартный нестовый ValidationPipe выбрасывать то, что нам хочется можно просто передав функцию-фабрику в конструктор:

app.useGlobalPipes(new ValidationPipe({  exceptionFactory: errors => new ValidationException(errors);   });)

Соответственно, на выходе наш ValidationException замапится на BadRequestException с кодом 400 - потому что он ClientException.

Другой пример, с NotFoundException:

export const EMPLOYEE_NOT_FOUND_ERROR_CODE = 50712;export class EmployeeNotFoundException extends NotFoundException {  constructor (employeeId: number) {  super(      EMPLOYEE_NOT_FOUND_ERROR_CODE,      `Employee id = ${employeeId} not found!`    );  }}

Не очень приятно писать такие классы - всегда возникает лёгкое чувство... бойлерплейта ) Но зато, как приятно их потом использовать!

// Вместо, что не даст нам ни кодов, ни типа - ничего:throw new Error('...тут мы должны сформировать внятное сообщение...')// Простоthrow new EmployeeNotFoundException(id);

Сценарий использования NotAllowedException похож на предыдущий. Пользовать может иметь доступ к роуту getEmployeeById, но не иметь права запрашивать определённые категории работников. Соответственно, мы в сервисе можем проверить его доступ и выкинуть ошибку такого вида:

export const EMPLOYEE_NOT_ALLOWED_ERROR_CODE = 40565;export class EmployeeNotAllowedException extends NotAllowedException {  constructor (userId: number, employeeId: number) {  super(      EMPLOYEE_NOT_ALLOWED_ERROR_CODE,      `User id = ${userId} is not allowed to query employee id = ${employeeId}!`    );  }}

Ну и наконец, все ошибки, в которых клиент не виноват: он имеет нужный доступ, запросил существующий ресурс и нигде не напутал с полями, и всё-таки облом - значит ServerException. Здесь я даже не буду особо приводить примеров - их множество. Но для клиента они всё одно - ошибка на сервере.

МАППИНГ

Мапятся внутренние ошибки на транспорто-специфичные в едином GlobalExceptionFilter, фильтр этот на вход получает один или несколько форматтеров. Задача форматтеров - преобразование вылетевшей ошибки в её конечный вид, можно сказать сериализация.

export interface IExceptionsFormatter {  // Verbose - флаг, который мы держим в конфигурации. Он используется для того,  // чтобы в девелоперской среде всегда на клиент отдавалась полная инфа о  // ошибке, а на проде - нет.  format (exception: unknown, verbose: boolean): unknown;    // При помощи этого метода можно понять, подходит ли данных форматтер  // для этого типа приложения или нет.  match (host: ArgumentsHost): boolean;}@Module({})export class ExceptionsModule {  public static forRoot (options: ExceptionsModuleOptions): DynamicModule {    return {      module: ExceptionsModule,      providers: [        ExceptionsModuleConfig,        {          provide: APP_FILTER,          useClass: GlobalExceptionsFilter        },        {          provide: 'FORMATTERS',          useValue: options.formatters        }      ]    };  }}const typesMap = new Map<string, number>().set('authentication', 401).set('authorization', 403).set('not_found', 404).set('client', 400).set('server', 500);@Catch()export class GlobalExceptionsFilter implements ExceptionFilter {  constructor (    @InjectLogger(GlobalExceptionsFilter) private readonly logger: ILogger,    @Inject('FORMATTERS') private readonly formatters: IExceptionsFormatter[],    private readonly config: ExceptionsModuleConfig  ) { }  catch (exception: Exception, argumentsHost: ArgumentsHost): Observable<any> {    this.logger.error(exception);    const formatter = this.formatters.find(x => x.match(argumentsHost));    const payload = formatter?.format(exception, this.config.verbose) || 'NO FORMATTER';    // В случае http мы ставим нужный статус-код и возвращаем ответ.if (argumentsHost.getType() === 'http') {      const request = argumentsHost.switchToHttp().getResponse();      const status = typesMap.get(exception.type) || 500;      request.status(status).send(payload);      return EMPTY;    }// В случае же RPC - бросаем дальше, транспорт разберётся.    return throwError(payload);  }}

Бывает конечно, что мы где-то напортачили и из сервиса вылетело что-то не унаследованное от Exception. На этот случай у нас есть ещё интерцептор, который все ошибки, не являющиеся экземплярами наследников Exception, заворачивает в new UnexpectedException(error) и прокидывает дальше. UnexpectedException естественно наследуется от ServerException. Для нас возникновение такой ошибки - иногда некритичный, но всё же баг, который фиксируется и исправляется.


В принципе, это всё. Для 95% наших задач этого вполне хватает. Способ может и не "канонический", но удобный и вполне рабочий - то, к чему мы и стремились.

И всё же бывают ситуации

КОГДА ВСЁ НЕ ТАК ЯСНО.

Приведу два примера:

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

В таких случаях всё-таки приходится проявлять "фантазию", например, обернуть в try/catch обработку каждой строки csv-файла. И в блоке catch писать ошибку в "отчёт". Тоже не бином Ньютона )

Второй. Я сознательно не написал выше реализацию DownloadException.

export class DOWNLOAD_ERROR_CODE = 5506;export class DownloadException extends ServerException {  constructor (url: string, inner: any) {    super(      DOWNLOAD_ERROR_CODE,      `Failed to download file from ${url}`,      inner    );  }}

Почему ServerException? Потому что, в общем случае, клиенту всё равно почему сервер не смог куда-то там достучаться. Для него это просто какая-то ошибка, в которой он не виноват.

Однако, теоретически может быть такая ситуация, что мы пытаемся скачать файл по ссылке, предоставленной клиентом. И тогда, в случае неудачи, клиент должен получить 400 или может быть 404, но не 500.

Э... ну да, мы не будем так делать - т. к. это как минимум не очень безопасно ) Это лишь иллюстрация того, что подход не идеален и можно в нём найти такого рода нестыковки.

ЗАКЛЮЧЕНИЕ

Большое спасибо всем, кто дочитал до конца. Не думаю, что я в этой статье кому-то "открыл Америку". И я, конечно, далёк от навязывания кому-либо, чего-либо. И всё же, надеюсь, что эта попытка структурировать работу с ошибками для кого-то послужит отправной точкой в разработке собственного подхода под собственные задачи.

P. S. К сожалению, опенсорс в нашей компании в процессе согласования, поэтому привести реальный используемый код я не могу. Когда будет такая возможность, мы выложим на github библиотеку, при помощи которой работаем с исключениями. А за одно и некоторые другие пакеты, которые могут оказаться кому-то полезными.

P. P. S. Буду очень благодарен, за комментарии, особенно если в них будут практические примеры ситуаций, в которых у нас могут возникнуть трудности при использовании данного подхода. С удовольствием, поучаствую в обсуждении.

Подробнее..

Перевод Приёмы и хитрости начинающего Java-программиста

05.10.2020 10:14:40 | Автор: admin

Небольшая коллекция практик, трюков и подсказок, с помощью которых вы сэкономите своё время при изучении Java и написании кода на этом языке программирования. Перевод статьиTop 25 Java Tricks, Tips, and Best Practices от специалистов Рексофт.

Организация работы

Чистый код

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

  • Правило 10-50-500. В одном пакете не может быть более 10 классов. Каждый метод должен быть короче 50 строк кода, а каждый класс короче 500 строк.

  • SOLID принципы.

  • Использование паттернов проектирования.

Работа с ошибками

Stack Trace (Трассировка стека)

Выявление ошибок это, пожалуй, самая трудоемкая часть процесса разработки на Java. Трассировка стека позволяет вам точно отслеживать, где именно в проекте возникла ошибка или исключение (exception).

import java.io.*;Exception e = ;java.io.StringWriter sw = new java.io.StringWriter();e.printStackTrace(new java.io.PrintWriter(sw));String trace = sw.getBuffer().toString();

NullPointerException

Исключения, возникающие из-заnullзначений (NullPointerException), довольно часто появляются при попытке вызвать метод у несуществующего объекта.

Возьмем для примера следующий код:

int noOfStudents = school.listStudents().count;private int getListOfStudents(File[] files) {if (files == null)  throw new NullPointerException("File list cannot be null");}

Примечание переводчика: а вот пример от меня как переводчика материала:

String anyString = null;if (anyString.equals("some string")) { // здесь будет null pointer exception, т.к. anyString == null// ...}

Дата и Время

System.currentTimeMillis или System.nanoTime?

В Java есть два стандартных способа проведения операций со временем, и не всегда ясно, какой из них следует выбрать.

МетодSystem.currentTimeMillis()возвращает текущее количество миллисекунд с начала эры Unix в формате Long. Его точность составляет от 1 до 15 тысячных долей секунды в зависимости от системы.

long startTime = System.currentTimeMillis();long estimatedTime = System.currentTimeMillis() - startTime;

МетодSystem.nanoTime()имеет точность до одной миллионной секунды (наносекунды) и возвращает текущее значение наиболее точного доступного системного таймера.

long startTime = System.nanoTime();long estimatedTime = System.nanoTime() - startTime;

Таким образом, методSystem.currentTimeMillis()лучше применять для отображения и синхронизации абсолютного времени, аSystem.nanoTime()для измерения относительных интервалов времени.

Валидация Даты из строки

Если необходимо достать объектDateиз обычной строки в Java, можете использовать небольшой утилитный класс, который приведен ниже. Он позаботится обо всех сложностях валидации и преобразовании строки в объектDate.

package net.viralpatel.java;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;public class DateUtil {// List of all date formats that we want to parse.// Add your own format here.private static List; dateFormats = new ArrayList() {{add(new SimpleDateFormat("M/dd/yyyy"));add(new SimpleDateFormat("dd.M.yyyy"));add(new SimpleDateFormat("M/dd/yyyy hh:mm:ss a"));add(new SimpleDateFormat("dd.M.yyyy hh:mm:ss a"));add(new SimpleDateFormat("dd.MMM.yyyy"));add(new SimpleDateFormat("dd-MMM-yyyy"));}};/** * Convert String with various formats into java.util.Date *  * @param input *            Date as a string * @return java.util.Date object if input string is parsed  * successfully else returns null */public static Date convertToDate(String input) {Date date = null;if(null == input) {return null;}for (SimpleDateFormat format : dateFormats) {try {format.setLenient(false);date = format.parse(input);} catch (ParseException e) {//Shhh.. try other formats}if (date != null) {break;}}return date;}}

Пример его использования:

package net.viralpatel.java;public class TestDateUtil {public static void main(String[] args) {System.out.println("10/14/2012" + " = " + DateUtil.convertToDate("10/14/2012"));System.out.println("10-Jan-2012" + " = " + DateUtil.convertToDate("10-Jan-2012"));System.out.println("01.03.2002" + " = " + DateUtil.convertToDate("01.03.2002"));System.out.println("12/03/2010" + " = " + DateUtil.convertToDate("12/03/2010"));System.out.println("19.Feb.2011" + " = " + DateUtil.convertToDate("19.Feb.2011" ));System.out.println("4/20/2012" + " = " + DateUtil.convertToDate("4/20/2012"));System.out.println("some string" + " = " + DateUtil.convertToDate("some string"));System.out.println("123456" + " = " + DateUtil.convertToDate("123456"));System.out.println("null" + " = " + DateUtil.convertToDate(null));}}

Результат:

10/14/2012 = Sun Oct 14 00:00:00 CEST 201210-Jan-2012 = Tue Jan 10 00:00:00 CET 201201.03.2002 = Fri Mar 01 00:00:00 CET 200212/03/2010 = Fri Dec 03 00:00:00 CET 201019.Feb.2011 = Sat Feb 19 00:00:00 CET 20114/20/2012 = Fri Apr 20 00:00:00 CEST 2012some string = null123456 = nullnull = null

Строки

Оптимизация строки

Для конкатенации (сложения) строк в Java используется оператор +, для примера, в циклеforновый объект может создаваться для каждой новой строки, что приводит к потере памяти и увеличению времени работы программы.

Необходимо избегать создания Java строк через конструктор, пример:

// медленное создание строкиString bad = new String ("Yet another string object");// быстрое создание строкиString good = "Yet another string object"

Одинарные и двойные кавычки

Что ты ожидаешь в результате выполнения этого кода?

public class Haha {  public static void main(String args[]) {    System.out.print("H" + "a");    System.out.print('H' + 'a');  }}

Казалось бы, строка должна возвращать HaHa, но на самом деле это будет Ha169.

Двойные кавычки обрабатывают символы как строки, но одинарные кавычки ведут себя иначе. Они преобразуют символьные операнды ('H'и'a') в целые значения посредством расширения примитивных типов получается 169.

Математика

Float или Double?

Программисты часто не могут выбрать необходимую точность для чисел с плавающей запятой. Float требует всего 4 байта, но имеет только 7 значащих цифр, а Double в два раза точнее (15 цифр), но в два раза прожорливее.

Фактически, большинство процессоров могут одинаково эффективно работать как с Float, так и с Double, поэтому воспользуйтесь рекомендацией Бьорна Страуструпа (автор языка С++):

Выбор правильной точности для решения реальных задач требует хорошего понимания природы машинных вычислений. Если у вас его нет, либо посоветуйтесь с кем-нибудь, либо изучите проблему самостоятельно, либо используйте Double и надейтесь на лучшее.

Проверка на нечетность

Можно ли использовать этот код для точного определения нечетного числа?

public boolean oddOrNot(int num) {  return num % 2 == 1;}

Надеюсь, вы заметили хитрость. Если мы решим таким образом проверить отрицательное нечетное число (например, -5), остаток от деления не будет равен единице, поэтому воспользуйтесь более точным методом:

public boolean oddOrNot(int num) {  return (num & 1) != 0;}

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

Возведение в степень

Возвести число в степень можно двумя способами:

  1. простое умножение;

  2. используя методMath.pow()(двойное основание, двойной показатель степени).

Использование библиотечной функции рекомендуется только в случае крайней необходимости, например, в случае дробной или отрицательной степени.

double result = Math.pow(625, 0.5); // 25.0

Простое умножение в Java работает в 300-600 раз эффективнее, кроме того, его можно дополнительно оптимизировать:

double square = double a * double a;  double cube = double a * double a * double a; // не оптимизированоdouble cube = double a * double square;  // оптимизировано double quad = double a * double a * double a * double a; // не оптимизированоdouble quad = double square * double square; // оптимизировано

JIT оптимизация

Код Java обрабатывается с использованием JIT-компиляции: сначала он транслируется в платформенно-независимый байт-код, а затем в машинный код. При этом оптимизируется все возможное, и разработчик может помочь компилятору создать максимально эффективную программу.

В качестве примера рассмотрим две простые операции:

// 1n += 2 * i * i; // 2n += 2 * (i * i);

Давайте измерим время выполнения каждого из них:

// 1long startTime1 = System.nanoTime(); int n1 = 0; for (int i = 0; i < 1000000000; i++) {   n1 += 2 * i * i; } double resultTime1 = (double)(System.nanoTime() - startTime1) / 1000000000;System.out.println(resultTime1 + " s");  // 2long startTime2 = System.nanoTime(); int n2 = 0; for (int i = 0; i < 1000000000; i++) {   n2 += 2 * (i * i); } double resultTime2 = (double)(System.nanoTime() - startTime2) / 1000000000;System.out.println(resultTime2 + " s");

Запустив этот код несколько раз, мы получим примерно следующее:

|`2*(i*i)`| `2*i*i`||---------|--------||0.5183738 | 0.6246434||0.5298337 | 0.6049722||0.5308647 | 0.6603363||0.5133458 | 0.6243328||0.5003011 | 0.6541802||0.5366181 | 0.6312638||0.515149  | 0.6241105||0.5237389 | 0.627815 ||0.5249942 | 0.6114252||0.5641624 | 0.6781033||0.538412  | 0.6393969||0.5466744 | 0.6608845||0.531159  | 0.6201077||0.5048032 | 0.6511559||0.5232789 | 0.6544526|

Схема очевидна: группировка переменных в круглые скобки ускоряет работу программы. Это связано с генерацией более эффективного байт-кода при умножении одинаковых значений.

Вы можете узнать больше об этом экспериментездесь. Или можете провести свой собственный тест, используя онлайн-компилятор Java.

Структуры данных

Комбинирование хеш-таблиц

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

import java.util.*;Map m1 = ;Map m2 = ;m2.putAll(m1); // добавить к m2 все элементы из m1

Array или ArrayList?

Выбор междуArrayиArrayListзависит от специфики задачи Java, которую вы хотите решить. Запомните следующие особенности этих типов:

  • Массив имеет фиксированный размер, и память для него выделяется во время объявления, а размерArrayListможет динамически меняться.

  • Массивы Java работают намного быстрее, а вArrayListнамного проще добавлять и удалять элементы.

  • При работе сArrayскорее всего возникнет ошибкаArrayIndexOutOfBoundsException.

  • ArrayList может быть только одномерным, когда массивы Java могут быть многомерными.

import java.util.*; public class ArrayVsArrayList {  public static void main(String[] args) {    // создаем массив    int[] myArray = new int[6];     // ссылаемся на несуществующий индекс    myArray[7]= 10; // ArrayIndexOutOfBoundsException     // создаем ArrayList    List myArrayList = new ArrayList<>();     // простое добавление и удаление элементов    myArrayList.add(1);    myArrayList.add(2);    myArrayList.add(3);    myArrayList.add(4);    myArrayList.add(5);    myArrayList.remove(0);     // перебор элементов ArrayList     for(int i = 0; i < myArrayList.size(); i++) {      System.out.println("Element: " + myArrayList.get(i));    }     //  многомерный массив    int[][][] multiArray = new int[3][3][3];   }}

JSON

Сериализация и Десериализация

JSON невероятно удобный и полезный синтаксис для хранения и обмена данными. Java полностью поддерживает это.

Примечание переводчика: для использования JSON из примера необходимо подключить библиотекуJSON Simple.

Вы можете сериализовать данные следующим образом:

import org.json.simple.JSONObject;import org.json.simple.JSONArray; public class JsonEncodeDemo {  public static void main(String[] args) {    JSONObject obj = new JSONObject();    obj.put("Novel Name", "Godaan");    obj.put("Author", "Munshi Premchand");     JSONArray novelDetails = new JSONArray();    novelDetails.add("Language: Hindi");    novelDetails.add("Year of Publication: 1936");    novelDetails.add("Publisher: Lokmanya Press");            obj.put("Novel Details", novelDetails);      System.out.print(obj);    }}

Получается следующая строка JSON:

{"Novel Name":"Godaan","Novel Details":["Language: Hindi","Year of Publication: 1936","Publisher: Lokmanya Press"],"Author":"Munshi Premchand"}

Десериализация в Java выглядит так:

import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;import java.util.Iterator; import org.json.simple.JSONArray;import org.json.simple.JSONObject;import org.json.simple.parser.JSONParser;import org.json.simple.parser.ParseException; public class JsonParseTest {  private static final String filePath = "//home//user//Documents//jsonDemoFile.json";      public static void main(String[] args) {    try {      // читаем json файл      FileReader reader = new FileReader(filePath);      JSONParser jsonParser = new JSONParser();      JSONObject jsonObject = (JSONObject)jsonParser.parse(reader);                  // берем данные из json объекта      Long id =  (Long) jsonObject.get("id");      System.out.println("The id is: " + id);                  String type = (String) jsonObject.get("type");      System.out.println("The type is: " + type);       String name = (String) jsonObject.get("name");      System.out.println("The name is: " + name);       Double ppu =  (Double) jsonObject.get("ppu");      System.out.println("The PPU is: " + ppu);       // извлекаем массив              System.out.println("Batters:");      JSONArray batterArray= (JSONArray) jsonObject.get("batters");      Iterator i = batterArray.iterator();            // проходимся по всем элементам массива      while (i.hasNext()) {        JSONObject innerObj = (JSONObject) i.next();        System.out.println("ID "+ innerObj.get("id") +             " type " + innerObj.get("type"));      }       System.out.println("Topping:");      JSONArray toppingArray= (JSONArray) jsonObject.get("topping");      Iterator j = toppingArray.iterator();      while (j.hasNext()) {        JSONObject innerObj = (JSONObject) j.next();        System.out.println("ID "+ innerObj.get("id") +             " type " + innerObj.get("type"));      }    } catch (FileNotFoundException|IOException|ParseException|NullPointerException ex) {      ex.printStackTrace();    }  }}

Используемый в примере файл JSON (jsonDemoFile.json):

{  "id": 0001,  "type": "donut",  "name": "Cake",  "ppu": 0.55,  "batters":    [      { "id": 1001, "type": "Regular" },      { "id": 1002, "type": "Chocolate" },      { "id": 1003, "type": "Blueberry" },      { "id": 1004, "type": "Devil's Food" }    ],  "topping":    [      { "id": 5001, "type": "None" },      { "id": 5002, "type": "Glazed" },      { "id": 5005, "type": "Sugar" },      { "id": 5007, "type": "Powdered Sugar" },      { "id": 5006, "type": "Chocolate with Sprinkles" },      { "id": 5003, "type": "Chocolate" },      { "id": 5004, "type": "Maple" }    ]}

Примечание переводчика: в Java проектах очень часто для работы с JSON используют библиотеки Gson от Google или Jackson. Обе библиотеки очень популярны и хорошо поддерживаются. Попробуйте и их.

Ввод и Вывод

FileOutputStream или FileWriter?

Запись файлов в Java осуществляется двумя способами:FileOutputStreamиFileWriter. Какой метод выбрать, зависит от конкретной задачи.

FileOutputStreamпредназначен для записи необработанных байтовых потоков. Это делает его идеальным решением, например, для работы с изображениями.

File foutput = new File(file_location_string);FileOutputStream fos = new FileOutputStream(foutput);BufferedWriter output = new BufferedWriter(new OutputStreamWriter(fos));output.write("Buffered Content");

УFileWriterдругое призвание: работа с потоками символов. Поэтому, если вы пишете текстовые файлы, выберите этот метод.

FileWriter fstream = new FileWriter(file_location_string);BufferedWriter output = new BufferedWriter(fstream);output.write("Buffered Content");

Производительность и лучшие практики

Пустая коллекция вместо Null

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

import java.util.*public List getLocations() {  List result = someService.getLocationsFromDB();  return result == null ? Collections.emptyList() : result;}

Создание объектов только когда необходимо

Создание объектов одна из самых затратных операций в Java. Лучше всего создавать их только тогда, когда они действительно нужны.

import java.util.ArrayList;import java.util.List; public class Employees {  private List Employees;   public List getEmployees() {    // initialization only if necessary    if(null == Employees) {      Employees = new ArrayList();    }        return Employees;  }}

Deadlocks (Дедлоки)

Взаимная блокировка (Deadlock) потоков может происходить по многим причинам, и полностью защититься от них в Java 8 очень сложно. Чаще всего, это происходит, когда один синхронизируемый объект ожидает ресурсов, которые заблокированы другим синхронизированным объектом.

Вот пример тупика этого потока:

public class DeadlockDemo {  public static Object addLock = new Object();  public static Object subLock = new Object();   public static void main(String args[]) {    MyAdditionThread add = new MyAdditionThread();    MySubtractionThread sub = new MySubtractionThread();    add.start();    sub.start();  }   private static class MyAdditionThread extends Thread {    public void run() {      synchronized (addLock) {        int a = 10, b = 3;        int c = a + b;        System.out.println("Addition Thread: " + c);        System.out.println("Holding First Lock...");        try { Thread.sleep(10); }        catch (InterruptedException e) {}        System.out.println("Addition Thread: Waiting for AddLock...");        synchronized (subLock) {           System.out.println("Threads: Holding Add and Sub Locks...");        }      }    }  }   private static class MySubtractionThread extends Thread {    public void run() {      synchronized (subLock) {        int a = 10, b = 3;        int c = a - b;        System.out.println("Subtraction Thread: " + c);        System.out.println("Holding Second Lock...");        try { Thread.sleep(10); }        catch (InterruptedException e) {}        System.out.println("Subtraction  Thread: Waiting for SubLock...");        synchronized (addLock) {           System.out.println("Threads: Holding Add and Sub Locks...");        }      }    }  }}

Результат этой программы:

=====Addition Thread: 13Subtraction Thread: 7Holding First Lock...Holding Second Lock...Addition Thread: Waiting for AddLock...Subtraction  Thread: Waiting for SubLock...

Взаимоблокировок можно избежать, изменив порядок вызова потоков:

private static class MySubtractionThread extends Thread {  public void run() {    synchronized (addLock) {      int a = 10, b = 3;      int c = a - b;      System.out.println("Subtraction Thread: " + c);      System.out.println("Holding Second Lock...");      try { Thread.sleep(10); }      catch (InterruptedException e) {}      System.out.println("Subtraction  Thread: Waiting for SubLock...");      synchronized (subLock) {        System.out.println("Threads: Holding Add and Sub Locks...");      }    }  }}

Вывод:

=====Addition Thread: 13Holding First Lock...Addition Thread: Waiting for AddLock...Threads: Holding Add and Sub Locks...Subtraction Thread: 7Holding Second Lock...Subtraction  Thread: Waiting for SubLock...Threads: Holding Add and Sub Locks...

Резервирование памяти

Некоторые приложения Java очень ресурсоемки и могут работать медленно. Для повышения производительности вы можете выделить на машине Java больше оперативной памяти.

export JAVA_OPTS="$JAVA_OPTS -Xms5000m -Xmx6000m -XX:PermSize=1024m -XX:MaxPermSize=2048m"
  • Xms минимальный пул выделения памяти;

  • Xmx максимальный пул выделения памяти;

  • XX: PermSize начальный размер, который будет выделен при запуске JVM;

  • XX: MaxPermSize максимальный размер, который можно выделить при запуске JVM.

Примечание переводчика: PermSize и MaxPermSize, начиная с Java 8 уже больше не используются.

Решение распространенных проблем

Содержимое директории

Java позволяет вам получать имена всех подкаталогов и файлов в папке в виде массива, который затем можно последовательно прочитать:

import java.io.*; public class ListContents {  public static void main(String[] args) {    File file = new File("//home//user//Documents/");    String[] files = file.list();     System.out.println("Listing contents of " + file.getPath());    for(int i=0 ; i < files.length ; i++) {      System.out.println(files[i]);    }  }}

Выполнение консольных команд

Java позволяет выполнять консольные команды прямо из кода, используя классRuntime. Очень важно не забывать об обработке исключений.

Например, давайте попробуем открыть файл PDF через терминал Java (на Linuxe):

public class ShellCommandExec {  public static void main(String[] args) {    String gnomeOpenCommand = "gnome-open //home//user//Documents//MyDoc.pdf";     try {      Runtime rt = Runtime.getRuntime();      Process processObj = rt.exec(gnomeOpenCommand);       InputStream stdin = processObj.getErrorStream();      InputStreamReader isr = new InputStreamReader(stdin);      BufferedReader br = new BufferedReader(isr);       String myoutput = "";       while ((myoutput=br.readLine()) != null) {        myoutput = myoutput+"\n";      }      System.out.println(myoutput);    } catch (Exception e) {      e.printStackTrace();    }  }

Воспроизведение звуков

Звук важный компонент многих десктопных приложений и игр. Язык программирования Java предоставляет средства для работы с ним.

import java.io.*;import java.net.URL;import javax.sound.sampled.*;import javax.swing.*; public class PlaySoundDemo extends JFrame {    // constructor   public playSoundDemo() {      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);      this.setTitle("Play Sound Demo");      this.setSize(300, 200);      this.setVisible(true);       try {         URL url = this.getClass().getResource("MyAudio.wav");         AudioInputStream audioIn = AudioSystem.getAudioInputStream(url);         Clip clip = AudioSystem.getClip();         clip.open(audioIn);         clip.start();      } catch (UnsupportedAudioFileException|IOException|LineUnavailableException e) {         e.printStackTrace();      }   }    public static void main(String[] args) {      new PlaySoundDemo();   }}

Отправка email

Отправить электронную почту на Java очень просто. Вам просто нужно установитьJava Mailи указать путь к нему в пути к классам проекта.

import java.util.*;import javax.mail.*;import javax.mail.internet.*; public class SendEmail {  public static void main(String [] args) {        String to = "recipient@gmail.com";    String from = "sender@gmail.com";    String host = "localhost";     Properties properties = System.getProperties();    properties.setProperty("mail.smtp.host", host);    Session session = Session.getDefaultInstance(properties);     try{      MimeMessage message = new MimeMessage(session);      message.setFrom(new InternetAddress(from));       message.addRecipient(Message.RecipientType.TO,new InternetAddress(to));       message.setSubject("My Email Subject");      message.setText("My Message Body");      Transport.send(message);      System.out.println("Sent successfully!");    } catch (MessagingException ex) {      ex.printStackTrace();    }  }}

Получение координат курсора

Чтобы фиксировать события мыши, вам необходимо реализовать интерфейсMouseMotionListener. Когда курсор попадает в определенную область, срабатывает обработчик событияmouseMoved, из которого вы можете получить точные координаты (используя Swing для UI)

import java.awt.event.*;import javax.swing.*; public class MouseCaptureDemo extends JFrame implements MouseMotionListener {  public JLabel mouseHoverStatus;   public static void main(String[] args) {    new MouseCaptureDemo();  }   MouseCaptureDemo() {    setSize(500, 500);    setTitle("Frame displaying Coordinates of Mouse Motion");     mouseHoverStatus = new JLabel("No Mouse Hover Detected.", JLabel.CENTER);    add(mouseHoverStatus);    addMouseMotionListener(this);    setVisible(true);  }   public void mouseMoved(MouseEvent e) {    mouseHoverStatus.setText("Mouse Cursor Coordinates => X:"+e.getX()+" | Y:"+e.getY());  }   public void mouseDragged(MouseEvent e) {  }}
Подробнее..

Перевод Как определять собственные классы исключений в Python

16.01.2021 18:16:46 | Автор: admin
Привет, Хабр!

Ваш интерес к новой книге "Секреты Python Pro" убедил нас, что рассказ о необычностях Python заслуживает продолжения. Сегодня предлагаем почитать небольшой туториал о создании кастомных (в тексте собственных) классах исключений. У автора получилось интересно, сложно не согласиться с ним в том, что важнейшим достоинством исключения является полнота и ясность выдаваемого сообщения об ошибке. Часть кода из оригинала в виде картинок.

Добро пожаловать под кат


Создание собственных классов ошибок


В Python предусмотрена возможность создавать собственные классы исключений. Создавая такие классы, можно разнообразить дизайн классов в приложении. Собственный класс ошибок мог бы логировать ошибки, инспектировать объект. Это мы определяем, что делает класс исключений, хотя, обычно собственный класс едва ли сможет больше, чем просто отобразить сообщение.

Естественно, важен и сам тип ошибки, и мы часто создаем собственные типы ошибок, чтобы обозначить конкретную ситуацию, которая обычно не покрывается на уровне языка Python. Таким образом, пользователи класса, встретив такую ошибку, будут в точности знать, что происходит.

Эта статья состоит из двух частей. Сначала мы определим класс исключений сам по себе. Затем продемонстрируем, как можно интегрировать собственные классы исключений в наши программы на Python и покажем, как таким образом повысить удобство работы с теми классами, что мы проектируем.

Собственный класс исключений MyCustomError


При выдаче исключения требуются методы __init__() и __str__().
При выдаче исключения мы уже создаем экземпляр исключения и в то же время выводим его на экран. Давайте детально разберем наш собственный класс исключений, показанный ниже.



В вышеприведенном классе MyCustomError есть два волшебных метода, __init__ и __str__, автоматически вызываемых в процессе обработки исключений. Метод Init вызывается при создании экземпляра, а метод str при выводе экземпляра на экран. Следовательно, при выдаче исключения два этих метода обычно вызываются сразу друг за другом. Оператор вызова исключения в Python переводит программу в состояние ошибки.

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

В нашем случае можно сказать, что, если конструктору MyCustomError были переданы какие-либо аргументы, то мы берем первый переданный аргумент и присваиваем его атрибуту message в объекте. Если ни одного аргумента передано не было, то атрибуту message будет присвоено значение None.

В первом примере исключение MyCustomError вызывается без каких-либо аргументов, поэтому атрибуту message этого объекта присваивается значение None. Будет вызван метод str, который выведет на экран сообщение MyCustomError message has been raised.



Исключение MyCustomError выдается без каких-либо аргументов (скобки пусты). Иными словами, такая конструкция объекта выглядит нестандартно. Но это просто синтаксическая поддержка, оказываемая в Python при выдаче исключения.

Во втором примере MyCustomError передается со строковым аргументом We have a problem. Он устанавливается в качестве атрибута message у объекта и выводится на экран в виде сообщения об ошибке, когда выдается исключение.



Код для класса исключения MyCustomError находится здесь.

class MyCustomError(Exception):    def __init__(self, *args):        if args:            self.message = args[0]        else:            self.message = None    def __str__(self):        print('calling str')        if self.message:            return 'MyCustomError, {0} '.format(self.message)        else:            return 'MyCustomError has been raised'# выдача MyCustomErrorraise MyCustomError('We have a problem')


Класс CustomIntFloatDic


Создаем собственный словарь, в качестве значений которого могут использоваться только целые числа и числа с плавающей точкой.

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

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

Создавая собственный словарь, нужно учитывать, что в нем есть два места, где в словарь могут добавляться значения. Во-первых, это может происходить в методе init при создании объекта (на данном этапе объекту уже могут быть присвоены ключи и значения), а во-вторых при установке ключей и значений прямо в словаре. В обоих этих местах требуется написать код, гарантирующий, что значение может относиться только к типу int или float.

Для начала определю класс CustomIntFloatDict, наследующий от встроенного класса dict. dict передается в списке аргументов, которые заключены в скобки и следуют за именем класса CustomIntFloatDict.

Если создан экземпляр класса CustomIntFloatDict, причем, параметрам ключа и значения не передано никаких аргументов, то они будут установлены в None. Выражение if интерпретируется так: если или ключ равен None, или значение равно None, то с объектом будет вызван метод get_dict(), который вернет атрибут empty_dict; такой атрибут у объекта указывает на пустой список. Помните, что атрибуты класса доступны у всех экземпляров класса.



Назначение этого класса позволить пользователю передать список или кортеж с ключами и значениями внутри. Если пользователь вводит список или кортеж в поисках ключей и значений, то два эти перебираемых множества будут сцеплены при помощи функции zip языка Python. Подцепленная переменная, указывающая на объект zip, поддается перебору, а кортежи поддаются распаковке. Перебирая кортежи, я проверяю, является ли val экземпляром класса int или float. Если val не относится ни к одному из этих классов, я выдаю собственное исключение IntFloatValueError и передаю ему val в качестве аргумента.

Класс исключений IntFloatValueError


При выдаче исключения IntFloatValueError мы создаем экземпляр класса IntFloatValueError и одновременно выводим его на экран. Это означает, что будут вызваны волшебные методы init и str.

Значение, спровоцировавшее выдаваемое исключение, устанавливается в качестве атрибута value, сопровождающего класс IntFloatValueError. При вызове волшебного метода str пользователь получает сообщение об ошибке, информирующее, что значение init в CustomIntFloatDict является невалидным. Пользователь знает, что делать для исправления этой ошибки.



Классы исключений IntFloatValueError и KeyValueConstructError



Если ни одно исключение не выдано, то есть, все val из сцепленного объекта относятся к типам int или float, то они будут установлены при помощи __setitem__(), и за нас все сделает метод из родительского класса dict, как показано ниже.


Класс KeyValueConstructError


Что произойдет, если пользователь введет тип, не являющийся списком или кортежем с ключами и значениями?

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

Если пользователь не укажет ключи и значения как список или кортеж, то будет выдано исключение KeyValueConstructError. Цель этого исключения проинформировать пользователя, что для записи ключей и значений в объект CustomIntFloatDict, список или кортеж должен быть указан в конструкторе init класса CustomIntFloatDict.

В вышеприведенном примере, в качестве второго аргумента конструктору init было передано множество, и из-за этого было выдано исключение KeyValueConstructError. Польза выведенного сообщения об ошибке в том, что отображаемое сообщение об ошибке информирует пользователя: вносимые ключи и значения должны сообщаться в качестве либо списка, либо кортежа.

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

Далее я даже включаю типы данных, присущие объектам, добавленным к конструктору init делаю это для большей ясности.



Установка ключа и значения в CustomIntFloatDict


CustomIntFloatDict наследует от dict. Это означает, что он будет функционировать в точности как словарь, везде за исключением тех мест, которые мы выберем для точечного изменения его поведения.

__setitem__ это волшебный метод, вызываемый при установке ключа и значения в словаре. В нашей реализации setitem мы проверяем, чтобы значение относилось к типу int или float, и только после успешной проверки оно может быть установлено в словаре. Если проверка не пройдена, то можно еще раз воспользоваться классом исключения IntFloatValueError. Здесь можно убедиться, что, попытавшись задать строку bad_value в качестве значения в словаре test_4, мы получим исключение.



Весь код к этому руководству показан ниже и выложен на Github.

# Создаем словарь, значениями которого могут служить только числа типов int и float  class IntFloatValueError(Exception):    def __init__(self, value):        self.value = value    def __str__(self):        return '{} is invalid input, CustomIntFloatDict can only accept ' \               'integers and floats as its values'.format(self.value)class KeyValueContructError(Exception):    def __init__(self, key, value):        self.key = key        self.value = value    def __str__(self):        return 'keys and values need to be passed as either list or tuple' + '\n' + \                ' {} is of type: '.format(self.key) + str(type(self.key)) + '\n' + \                ' {} is of type: '.format(self.value) + str(type(self.value))class CustomIntFloatDict(dict):    empty_dict = {}    def __init__(self, key=None, value=None):        if key is None or value is None:            self.get_dict()        elif not isinstance(key, (tuple, list,)) or not isinstance(value, (tuple, list)):            raise KeyValueContructError(key, value)        else:            zipped = zip(key, value)            for k, val in zipped:                if not isinstance(val, (int, float)):                    raise IntFloatValueError(val)                dict.__setitem__(self, k, val)    def get_dict(self):        return self.empty_dict    def __setitem__(self, key, value):        if not isinstance(value, (int, float)):            raise IntFloatValueError(value)        return dict.__setitem__(self, key, value)# тестирование # test_1 = CustomIntFloatDict()# print(test_1)# test_2 = CustomIntFloatDict({'a', 'b'}, [1, 2])# print(test_2)# test_3 = CustomIntFloatDict(('x', 'y', 'z'), (10, 'twenty', 30))# print(test_3)# test_4 = CustomIntFloatDict(('x', 'y', 'z'), (10, 20, 30))# print(test_4)# test_4['r'] = 1.3# print(test_4)# test_4['key'] = 'bad_value'


Заключение


Если создавать собственные исключения, то работать с классом становится гораздо удобнее. В классе исключения должны быть волшебные методы init и str, автоматически вызываемые в процессе обработки исключений. Только от вас зависит, что именно будет делать ваш собственный класс исключений. Среди показанных методов такие, что отвечают за инспектирование объекта и вывод на экран информативного сообщения об ошибке. Как бы то ни было, классы исключений значительно упрощают обработку всех возникающих ошибок!
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru