Путь Java-инженера GitHub 18k Star к тому, чтобы стать богом, разве вы не хотите узнать об этом!
Я полагаю, что многие люди знакомы с аннотациями в Java, такими как @Override, @Autowired, @Service и т. д., которые мы часто используем, которые предоставляются JDK или такими фреймворками, как Spring.
В процессе прошлого собеседования я обнаружил, что многие программисты знают об аннотациях только на уровне использования, и мало кто знает, как реализуются аннотации, не говоря уже об использовании пользовательских аннотаций для решения практических задач.
Но на самом деле, я думаю, что стандарт хорошего программиста — знать, как оптимизировать свой собственный код.С точки зрения оптимизации кода, как упростить код и удалить повторяющийся код, является важной темой.В этой тематической области пользовательские аннотации абсолютно можно считать великим героем.
так,На мой взгляд, использование пользовательских аннотаций — хорошие программисты.
Итак, в этой статье я представлю несколько примеров, которые автор действительно использует в разработке, и покажу вам, как использовать аннотации для улучшения качества вашего кода.
базовые знания
В Java есть два типа аннотаций: метааннотации и пользовательские аннотации.
Многие ошибочно думают, что пользовательские аннотации определяются самими разработчиками, а те, которые предоставляются другими фреймворками, не учитываются, но на самом деле аннотации, о которых мы упоминали выше, на самом деле являются пользовательскими аннотациями.
В мире программирования есть много описаний «мета», таких как «мета-аннотация», «мета-данные», «мета-класс», «мета-таблица» и т. д. «Мета» здесь на самом деле переводится из «мета».
Обычно мы ставимПод метааннотациями понимаются аннотации, описывающие аннотации.,Под метаданными понимаются данные, описывающие данные,Под метаклассом понимается класс, описывающий класс...
Поэтому в Java, за исключением ограниченного числа фиксированных «аннотаций, описывающих аннотацию», все аннотации являются пользовательскими аннотациями.
В JDK предусмотрено четыре стандартных класса аннотаций (мета-аннотаций) для аннотирования типов аннотаций:
@Target
@Retention
@Documented
@Inherited
За исключением вышеупомянутых четырех, все остальные аннотации являются пользовательскими аннотациями.
Я не буду здесь подробно описывать функции четырех вышеприведенных метааннотаций, вы можете изучить их самостоятельно.
Все примеры, упомянутые в этой статье, представляют собой сценарии, которые автор фактически использует в своей повседневной работе.У этих примеров есть одна общая черта: все они используют технологию АОП Spring.
Я полагаю, что многие люди знают, что такое АОП и как его использовать, поэтому я не буду его здесь представлять.
Ведение журнала с пользовательскими аннотациями
Не знаю, сталкивались ли вы с подобными требованиями, то есть вы хотите сделать унифицированную обработку лога на входе или выходе из метода, например запись входных параметров, выходных параметров, запись времени выполнения метода и т.д.
Если писать такой код самостоятельно в каждом методе, с одной стороны, будет много дублирования кода, а с другой стороны, его легко пропустить.
В этом сценарии вы можете использовать пользовательские аннотации + аспекты для достижения этой функции.
Предположим, мы хотим записать, что операция сделала с некоторыми методами веб-запроса, например добавление новой записи или удаление записи.
Сначала мы определяем аннотацию:
/**
* Operate Log 的自定义注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpLog {
/**
* 业务类型,如新增、删除、修改
*
* @return
*/
public OpType opType();
/**
* 业务对象名称,如订单、库存、价格
*
* @return
*/
public String opItem();
/**
* 业务对象编号表达式,描述了如何获取订单号的表达式
*
* @return
*/
public String opItemIdExpression();
}
Потому что нам нужно не только зафиксировать в журнале то, что было оперировано на этот раз, но и знать конкретный уникальный идентификатор оперируемого объекта, например информацию о номере заказа.
Однако типы параметров каждого метода интерфейса определенно отличаются, и единый стандарт иметь сложно, тогда можно использовать выражение Spel, то есть указать, как получить уникальный идентификатор соответствующего объекта в выражении.
С приведенными выше аннотациями вы можете написать аспект. Основной код выглядит следующим образом:
/**
* OpLog的切面处理类,用于通过注解获取日志信息,进行日志记录
*
* @author Hollis
*/
@Aspect
@Component
public class OpLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(OpLogAspect.class);
@Autowired
HttpServletRequest request;
@Around("@annotation(com.hollis.annotation.OpLog)")
public Object log(ProceedingJoinPoint pjp) throws Exception {
Method method = ((MethodSignature)pjp.getSignature()).getMethod();
OpLog opLog = method.getAnnotation(OpLog.class);
Object response = null;
try {
// 目标方法执行
response = pjp.proceed();
} catch (Throwable throwable) {
throw new Exception(throwable);
}
if (StringUtils.isNotEmpty(opLog.opItemIdExpression())) {
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(opLog.opItemIdExpression());
EvaluationContext context = new StandardEvaluationContext();
// 获取参数值
Object[] args = pjp.getArgs();
// 获取运行时参数的名称
LocalVariableTableParameterNameDiscoverer discoverer
= new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
// 将参数绑定到context中
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
}
// 将方法的resp当做变量放到context中,变量名称为该类名转化为小写字母开头的驼峰形式
if (response != null) {
context.setVariable(
CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, response.getClass().getSimpleName()),
response);
}
// 解析表达式,获取结果
String itemId = String.valueOf(expression.getValue(context));
// 执行日志记录
handle(opLog.opType(), opLog.opItem(), itemId);
}
return response;
}
private void handle(OpType opType, String opItem, String opItemId) {
// 通过日志打印输出
LOGGER.info("opType = " + opType.name() +",opItem = " +opItem + ",opItemId = " +opItemId);
}
}
В приведенном выше разделе есть несколько моментов, на которые необходимо обратить внимание:
1. Используйте аннотацию @Around, чтобы указать, что аспект установлен для метода, отмеченного OpLog. 2. Используйте связанные методы Spel для получения уникального идентификатора целевого объекта из соответствующих параметров через указанное представление. 3. После успешного выполнения метода выведите лог.
С вышеупомянутыми аспектами и аннотациями нам нужно только добавить аннотации к соответствующим методам, таким как:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#id")
public @ResponseBody
HashMap view(@RequestParam(name = "id") String id)
throws Exception {
}
Вышеупомянутый уникальный идентификатор объекта для работы в списке параметров входного параметра, который можно использовать напрямую#id
Просто укажите.
Если уникальный идентификатор управляемого объекта отсутствует в списке параметров, он может быть атрибутом объекта ввода параметра.Использование следующее:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#orderVo.id")
public @ResponseBody
HashMap update(OrderVO orderVo)
throws Exception {
}
Вышеупомянутое можно получить из значения атрибута id входящего объекта OrderVO.
Что делать, если уникального идентификатора, который мы хотим записать, нет во входных параметрах? Наиболее типичным является метод вставки, до того, как вставка будет успешной, вы не знаете, что такое идентификатор первичного ключа, что мне делать?
В приведенном выше аспекте мы сделали одну вещь, то есть мы также будем использовать выражение для анализа возвращаемого значения метода, Если конкретное значение может быть проанализировано, это может быть. Это написано следующим образом:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#insertResult.id")
public @ResponseBody
InsertResult insert(OrderVO orderVo)
throws Exception {
return orderDao.insert(orderVo);
}
Выше приведен простой сценарий ведения журнала с использованием пользовательских аннотаций + аспектов. Давайте посмотрим, как использовать аннотации для проверки параметров метода.
Используйте пользовательские аннотации для предварительной проверки
Когда мы предоставляем интерфейс наружу, к некоторым параметрам предъявляются определенные требования, например, некоторые значения параметров не могут быть пустыми и т.д. В большинстве случаев нам нужно самостоятельно активно проверять, чтобы определить, является ли значение, переданное другой стороной, разумным.
Вот рекомендуемый способ использования HibernateValidator + пользовательская аннотация + AOP для реализации проверки параметров.
Во-первых, у нас будет определенный класс входных параметров, который определяется следующим образом:
public class User {
private String idempotentNo;
@NotNull(
message = "userName can't be null"
)
private String userName;
}
Выше укажите, что параметр userName не может быть нулевым.
Затем используйте валидатор hibernate, чтобы определить класс инструментов для проверки параметров.
/**
* 参数校验工具
*
* @author Hollis
*/
public class BeanValidator {
private static Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(true)
.buildValidatorFactory().getValidator();
/**
* @param object object
* @param groups groups
*/
public static void validateObject(Object object, Class<?>... groups) throws ValidationException {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (constraintViolations.stream().findFirst().isPresent()) {
throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());
}
}
}
Приведенный выше код проверит bean-компонент, и в случае сбоя будет выдано исключение ValidationException.
Затем определите аннотацию:
/**
* facade接口注解, 用于统一对facade进行参数校验及异常捕获
* <pre>
* 注意,使用该注解需要注意,该方法的返回值必须是BaseResponse的子类
* </pre>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Facade {
}
В этой аннотации нет параметров, она используется только для обозначения тех методов проверки параметров.
Затем определите срез:
/**
* Facade的切面处理类,统一统计进行参数校验及异常捕获
*
* @author Hollis
*/
@Aspect
@Component
public class FacadeAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);
@Autowired
HttpServletRequest request;
@Around("@annotation(com.hollis.annotation.Facade)")
public Object facade(ProceedingJoinPoint pjp) throws Exception {
Method method = ((MethodSignature)pjp.getSignature()).getMethod();
Object[] args = pjp.getArgs();
Class returnType = ((MethodSignature)pjp.getSignature()).getMethod().getReturnType();
//循环遍历所有参数,进行参数校验
for (Object parameter : args) {
try {
BeanValidator.validateObject(parameter);
} catch (ValidationException e) {
return getFailedResponse(returnType, e);
}
}
try {
// 目标方法执行
Object response = pjp.proceed();
return response;
} catch (Throwable throwable) {
return getFailedResponse(returnType, throwable);
}
}
/**
* 定义并返回一个通用的失败响应
*/
private Object getFailedResponse(Class returnType, Throwable throwable)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//如果返回值的类型为BaseResponse 的子类,则创建一个通用的失败响应
if (returnType.getDeclaredConstructor().newInstance() instanceof BaseResponse) {
BaseResponse response = (BaseResponse)returnType.getDeclaredConstructor().newInstance();
response.setSuccess(false);
response.setResponseMessage(throwable.toString());
response.setResponseCode(GlobalConstant.BIZ_ERROR);
return response;
}
LOGGER.error(
"failed to getFailedResponse , returnType (" + returnType + ") is not instanceof BaseResponse");
return null;
}
}
Вышеприведенный код аналогичен предыдущему аспекту, в основном определяет аспект, который будет единообразно обрабатывать все методы, помеченные @Facade, то есть перед запуском вызова метода выполняется проверка параметров, при неудачной проверке возвращается фиксированное значение. Для неудавшегося ответа особенно важно отметить, что причина, по которой здесь может быть возвращен фиксированный BaseResponse, заключается в том, что мы потребуем, чтобы все ответы наших внешних интерфейсов наследовали класс BaseResponse, который будет определять некоторые параметры по умолчанию, такие как как коды ошибок и т.д.
После этого вам останется только добавить соответствующие аннотации к методам, требующим проверки параметров:
@Facade
public TestResponse query(User user) {
}
Таким образом, с помощью приведенных выше аннотаций и аспектов мы можем иметь единый контроль над всеми внешними методами.
На самом деле, я много чего упустил в приведенном выше фасадном аспекте, который мы действительно используем, он может не только проверять параметры, но и делать много других вещей. Например, унифицированная обработка исключений, унифицированное преобразование кодов ошибок, время выполнения методов записи, входные и выходные параметры методов записи и так далее.
Короче говоря, используя фасеты + пользовательские аннотации, мы можем сделать много вещей единообразно. В дополнение к приведенным выше сценариям у нас есть много похожих вариантов использования, таких как:
Унифицированная обработка кэша. Например, некоторые операции требуют проверки кеша перед операцией и обновления кеша после операции. Это может быть обработано единообразно с помощью пользовательской аннотации + аспекта.
Код на самом деле тот же, и идея относительно проста: использовать пользовательские аннотации, чтобы отметить исчерпание или метод, который должен быть обработан аспектом, а затем вмешаться в процесс выполнения метода в аспекте, например как выполнение некоторых специальных операций до или после выполнения.
Использование этого метода может значительно сократить повторяющийся код, значительно повысить элегантность кода и облегчить его использование.
Но в то же время не стоит злоупотреблять, ведь аннотация вроде бы простая, а на самом деле внутри много логики, которую легко не заметить. Как я писал ранееSpring официально рекомендует использовать транзакции @Transactional, почему я этого не рекомендую!Как упоминалось в этой точке зрения, бездумное использование аспектов и аннотаций может привести к некоторым ненужным проблемам.
В любом случае, пользовательские аннотации — хорошее изобретение, которое может сократить объем повторяющегося кода. Быстро используйте его в своем проекте.
Об авторе:Hollis, человек с уникальным увлечением программированием, технический эксперт Alibaba, соавтор «Трех курсов для программистов» и автор серии статей «Дорога к Java-инженерам».
Если у вас есть комментарии, предложения или вы хотите пообщаться с автором, вы можете подписаться на официальный аккаунт [Hollis], оставьте мне сообщение прямо в фоновом режиме.