Java中什么是注解及如何实现自定义注解

本文介绍Java注解的基本概念及应用场景,特别是元注解的使用,并深入探讨Spring框架中如何利用注解实现请求映射及自定义注解配合AOP进行业务逻辑增强。

什么是注解

注解(annotation):元数据,一种代码级别的说明,如同一张标签;

  1. 可以携带参数;
  2. 在特定场景下由外部解析产生作用;(在Spring容器启动初始化后由反射解析产生作用)

元注解(用于自定义注解)

@Documented 注解是否将包含在JavaDoc中,用的比较少;

@Retention 什么时候使用该注解,可选值有:

  • source 在源代码阶段使用,如@Override,lombok的注解@Data、@EqualsAndHashCode等等。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
    String staticConstructor() default "";
}
  • class 在代码编辑阶段使用,用的比较少。

  • runtime 在代码运行期使用,这个很常见。比如swagger、mybatisplus、spring、jackson等等。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ApiModel {
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableId {
    String value() default "";

    IdType type() default IdType.NONE;
}

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
}

@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface JsonIgnoreProperties {
}

@Target 注解用在什么地方,自定义注解时比用。比如上面列举的几个示例都有使用。

  • TYPE 表示可以用来修饰类、接口、注解类型或枚举类型;
  • PACKAGE 可以用来修饰包;
  • PARAMETER 可以用来修饰参数;
  • ANNOTATION_TYPE 可以用来修饰注解类型;
  • FIELD 可以用来修饰属性(包括枚举的常量);
  • CONSTRUCTOR 可以用来修饰构造器;
  • LOCAL_VARIABLE 可以用来修饰局部变量;

@Inherited 是否允许子类继承该注解。


注解的解析

要获取类、方法或字段的注解信息,必须通过Java反射技术来获取Annotation对象。


自定义注解类

/**
 * 自定义缓存注解
 * 
 * @author wanglingqiang
 * @date 2020/2/1 下午3:53
 **/
@Retention(RetentionPolicy.RUNTIME)  //运行期使用
@Target({ElementType.TYPE, ElementType.METHOD})  //约束,只能配置在类和方法上
public @interface DoSomething {

    String key();  // 参数一,缓存的key

    String cacheName();  //参数二,缓存的逻辑名称

    boolean needLog() default false; //参数三,是否需要记录日志
}

业务类、业务方法中使用自定义的注解

/**
 * 用户信息操作服务类
 *
 * @author wanglingqiang
 * @date 2020/2/1 下午1:50
 */
@Service
//@DoSomething(key = "c-key", cacheName = "USER", needLog = true)
public class UserServiceImpl implements IUserService {

    private TUserMapper userMapper;

    private static final String CACHE_NAME = "USER";

    @Override
    //#id占位符,动态获取方法参数传递过来的id
    //cacheName引用静态常量
    @DoSomething(key = "#id", cacheName = CACHE_NAME, needLog = true)
    public TUser getUserById(Integer id) {
        System.out.println("----in----");
        TUser user = userMapper.selectByPrimaryKey(id);
        return user;
    }
}

测试通过反射获取Annotation对象

@Test
public void testAnno(){
    Class clazz = UserServiceImpl.class;
    Class anno = DoSomething.class;

    DoSomething annotation = null;
    if (clazz.isAnnotationPresent(anno)) {
        annotation = (DoSomething) clazz.getAnnotation(anno);

        System.out.println(annotation.key());
        System.out.println(annotation.cacheName());
        System.out.println(annotation.needLog());
    }

    Method[] methods = clazz.getMethods();
    for (Method method : methods) {
        if (method.isAnnotationPresent(anno)) {
            annotation = (DoSomething) method.getAnnotation(anno);

            System.out.println(annotation.key());
            System.out.println(annotation.cacheName());
            System.out.println(annotation.needLog());
        }
    }
}

SpringMVC中是怎么使用注解的

对于一个http请求,为什么SpringMVC能准确的找到对应的Controller的某个方法进行处理?


### 日常使用代码示例:
@RestController
@RequestMapping("/visitor")
@Api(value = "访客控制类", tags = "访客控制类")
public class VisitorController {

    @Autowired
    private IVisitorService visitorService;

    @ApiOperation(value = "根据id查询", notes = "根据id查询")
    @RequestMapping(value = "/getById", method = RequestMethod.GET)
    public Result<Visitor> getById(@RequestParam("id") String id) {
        return Result.ok(visitorService.getById(id));
    }
    
}

我们在Class类上添加@Controller或@RestController注解,在Method方法上添加@RequestMapping注解,然后就可以通过 “http://域名/项目名/visitor/getById?id=xxx” 访问我们Controller的某个方法了。


底层源码的实现流程:

处理器映射器HandlerMapping的实现
URL注册

http请求过来了,负责处理url的映射。HandlerMapping默认的实现是RequestMappingHandlerMapping。
在这里插入图片描述

RequestMappingHandlerMapping的抽象父类AbstractHandlerMethodMapping:

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {

	public void registerMapping(T mapping, Object handler, Method method) {
		if (logger.isTraceEnabled()) {
			logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
		}
		this.mappingRegistry.register(mapping, handler, method);
	}

	@Override
	public void afterPropertiesSet() {
		initHandlerMethods();
	}
	

}

抽象父类AbstractHandlerMethodMapping实现了InitializingBean接口,Spring容器初始化完后会执行afterPropertiesSet(),再调用initHandlerMethods()。

	/**
	 * Scan beans in the ApplicationContext, detect and register handler methods.
	 * @see #getCandidateBeanNames()
	 * @see #processCandidateBean
	 * @see #handlerMethodsInitialized
	 */
	protected void initHandlerMethods() {
		// 获取容器里所有的Bean作为候选Bean,然后循环处理
		for (String beanName : getCandidateBeanNames()) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				//判断是否是Controller、RequestMapping注解
				processCandidateBean(beanName);
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}
 
    /**
	 * Determine the names of candidate beans in the application context.
	 * @since 5.1
	 * @see #setDetectHandlerMethodsInAncestorContexts
	 * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors
	 */
	protected String[] getCandidateBeanNames() {
		return (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
				obtainApplicationContext().getBeanNamesForType(Object.class));
	}

	/**
	 * Determine the type of the specified candidate bean and call
	 * {@link #detectHandlerMethods} if identified as a handler type.
	 * <p>This implementation avoids bean creation through checking
	 * {@link org.springframework.beans.factory.BeanFactory#getType}
	 * and calling {@link #detectHandlerMethods} with the bean name.
	 * @param beanName the name of the candidate bean
	 * @since 5.1
	 * @see #isHandler
	 * @see #detectHandlerMethods
	 */
	protected void processCandidateBean(String beanName) {
		Class<?> beanType = null;
		try {
			beanType = obtainApplicationContext().getType(beanName);
		}
		catch (Throwable ex) {
			// An unresolvable bean type, probably from a lazy bean - let's ignore it.
			if (logger.isTraceEnabled()) {
				logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
			}
		}
		//isHandler()判断是否存在Controller、RequestMapping注解
		if (beanType != null && isHandler(beanType)) {
			//取出加了RequestMapping注解的Method,注册
			detectHandlerMethods(beanName);
		}
	}

	/**
	 * Look for handler methods in the specified handler bean.
	 * @param handler either a bean name or an actual handler instance
	 * @see #getMappingForMethod
	 */
	protected void detectHandlerMethods(Object handler) {
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());

		if (handlerType != null) {
			Class<?> userType = ClassUtils.getUserClass(handlerType);
			//取出加了RequestMapping注解的Method放入Map,Key是java.lang.reflect.Method,value是URL路径
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					});
			if (logger.isTraceEnabled()) {
				logger.trace(formatMappings(userType, methods));
			}
			methods.forEach((method, mapping) -> {
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				//注册
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}
	
	private final MappingRegistry mappingRegistry = new MappingRegistry();

	protected void registerHandlerMethod(Object handler, Method method, T mapping) {
		//注册后放入mappingLookup
		this.mappingRegistry.register(mapping, handler, method);
	}

	class MappingRegistry {

		private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

		//注册后放入mappingLookup,T是URL路径,value是封装的方法信息
		private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

		private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

		private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

		private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

		private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

		/**
		 * Return all mappings and handler methods. Not thread-safe.
		 * @see #acquireReadLock()
		 */
		public Map<T, HandlerMethod> getMappings() {
			return this.mappingLookup;
		}
		
	}
	

isHandler()判断是否是Controller、RequestMapping注解

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
		implements MatchableHandlerMapping, EmbeddedValueResolverAware {
		
	@Override
	protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
	}
	
}

public abstract class AnnotatedElementUtils {

	public static boolean hasAnnotation(AnnotatedElement element, Class<? extends Annotation> annotationType) {
		// Shortcut: directly present on the element, with no merging needed?
		if (AnnotationFilter.PLAIN.matches(annotationType) ||
				AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) {
			//Bean的class的isAnnotationPresent注解的class,判断Bean上是否存在指定注解
			return element.isAnnotationPresent(annotationType);
		}
		// Exhaustive retrieval of merged annotations...
		return findAnnotations(element).isPresent(annotationType);
	}

}

以上列举了关键步骤的源码,通过以上步骤注册完成,URL和方法的对应关系放到Map集合里面。


HttpServletRequest请求来了映射到对应方法

关键源码如下:

/**
	 * Look up a handler method for the given request.
	 */
	@Override
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		//获取URL地址
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
		request.setAttribute(LOOKUP_PATH, lookupPath);
		this.mappingRegistry.acquireReadLock();
		try {
			//通过URL找到对应的方法
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

现学现用,写代码示例

通常我们写的业务方法

在写代码前,先看看通常我们写的业务方法:

/**
 * 用户信息操作服务类
 *
 * @author wanglingqiang
 * @date 2020/2/1 下午1:50
 */
@Service
public class UserServiceImpl implements IUserService {

	@Resource
	private CacheService cacheService;
    @Autowired
    private ILogService logService;
    @Autowired
    private TUserMapper userMapper;

    private static final String CACHE_NAME = "USER";

    @Override
    public TUser getUserById(Integer id) throws Throwable {
        System.out.println("----in----");
        //1.根据id从缓存获取数据
        TUser cacheUser = cacheService.cacheResult(id, CACHE_NAME);
        //2.判断缓存中数据是否存在,存在的话直接返回
        if(cacheUser != null) {
			return cacheUser;
		}
		TUser user = null;
		try {
			//3.缓存中没有,从数据库查询
			user = userMapper.selectPrimaryKey(id);
		} catch (Exception e) {
			//4.查询数据库出现异常记录日志
			logService.errorLog(id, UserServiceImpl.class.getSimpleName(), "getUserById()", e.getMessage());
		}
		//5.查询数据库正常记录日志
		logService.infoLog(id, UserServiceImpl.class.getSimpleName(), "getUserById()", e.getMessage());
        //6.user不为空放入缓存
        if(user != null) {
			cacheService.cachePut(id, user, CACHE_NAME);
		}
        return user;
    }
}

代码的问题

这段代码的可以说是“通俗易懂”,所有的代码都写在一起,是我们最常见的代码。但它还有另一个特征是臃肿:

  • 业务代码与技术代码耦合,缓存是锦上添花的技术代码,可以没有
  • 业务代码与增值业务代码耦合,日志记录是增值代码,可以没有
  • 多余的依赖关系,方法的核心功能可以不依赖CacheService、ILogService

带来的问题:

  • 可读性差,代码示例中的代码相对简单且有注释,万一复杂且没注释呢
  • 可复用性差,缓存、日志部分是通用的,但每个方法里面都要重复写一遍
  • 可维护性差,代码都写在一起,容易改出bug
  • 灵活性差,日志方法如果加一个参数,那所有使用的地方都要一起改一遍
  • 运行期依赖,如果运行期缓存服务挂了,那核心功能也会受影响

改进的思路

  • 单一职责原则,一个类或者一个接口只负责唯一一项职责,尽量设计出功能单一的接口
  • 迪米特法则,对象应该对其他对象保持最少了解,如果没有必要直接调用就不要有依赖关系
  • Spring AOP增加+自定义注解

改进后的代码

大概步骤:

  1. 创建自定义注解@DoSomething
  2. 将注解加在需要简化的方法上,加在UserServiceImpl类的getUserById()上
  3. 增加配置,编写切面类
  4. 简化业务代码

applicationContext.xml配置文件打开AOP切面功能。

<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>

利用JDK8获取形参名称,pom.xml添加-parameters

<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<compilerArgument>-parameters</compilerArgument>
				</configuration>
			</plugin>
		</plugins>
	</build>

定义切面类:

/**
 * 定义切面做增强,实现注解的功能
 *
 * @author wanglingqiang
 * @date 2020/2/1 下午4:09
 **/
@Component  //首先得是Spring容器的Bean
@Aspect     //定义为切面
public class DoSomethingAspect {

    @Resource
    private CacheService cacheService;
    @Autowired
    private ILogService logService;

    /**
     * 环绕通知增强
     *
     * @param pjp 连接点,是对增强方法的封装
     * @param dst 自定义注解
     * @return Object
     * @throws Throwable
     */
    //@Around("execution * com.enjoylearning.anno.service.*.*(..)")         //指定切点,拦截包下面所有类的方法
    //@Around("@annotation(com.enjoylearning.anno.annotation.DoSomething)")   //进一步细化,拦截添加有DoSomething注解的切点方法
    @Around("@annotation(dst)")     //加参数DoSomething dst后,@annotation简化了,下面两行业可以注释掉
    public Object doAround(ProceedingJoinPoint pjp, DoSomething dst) throws Throwable {
//        Method method = ((MethodSignature) (pjp.getSignature())).getMethod();
//        DoSomething dst = (DoSomething) method.getAnnotation(DoSomething.class);

        String id = getKey(dst.key(), pjp); //dst.key()是#id,通过spel把占位符转换成实际值
        String cacheName = dst.cacheName(); //USER
        boolean needLog = dst.needLog();    //true

//        System.out.println(id);
//        System.out.println(cacheName);
//        System.out.println(needLog);


        //1.根据id从缓存获取数据
//        TUser cacheUser = cacheService.cacheResult(id, CACHE_NAME);
        //为了通用性,这里不用TUser用Object
        Object resultObj = cacheService.cacheResult(id, cacheName);
        //2.判断缓存中数据是否存在,存在的话直接返回
        if (resultObj != null) {
            return resultObj;
        }
        try {
            //3.缓存中没有,从数据库查询
            resultObj = pjp.proceed();  //真正的业务方法
        } catch (Exception e) {
            //4.查询数据库出现异常记录日志
            if (needLog) {
                //errorLog(id, UserServiceImpl.class.getSimpleName(), "getUserById", e.getMessage());
                errorLog(id, pjp.getSourceLocation().getWithinType().getName(), pjp.getSignature().getName(), e.getMessage());
            }
        }
        //5.查询数据库正常记录日志
        if (needLog) {
            infoLog(id, pjp.getSourceLocation().getWithinType().getName(), pjp.getSignature().getName(), resultObj.toString());
        }
        //6.user不为空放入缓存
        if (resultObj != null) {
            cacheService.cachePut(id, resultObj, cacheName); // jedis.put(key, result, cacheName);
        }
        return resultObj;
    }

    private String getKey(String key, ProceedingJoinPoint pjp) {
        //Method method = ((MethodSignature) (pjp.getSignature())).getMethod();
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        Parameter[] parameters = method.getParameters();    //利用jdk8获取形参名称getParameters(),或者使用spring的工具类
        String[] paramNames = new String[parameters.length];//Java Compiler和pom.xml添加-parameters
        for (int i = 0; i < parameters.length; i++) {
//            System.out.println(""+parameters[i].isNamePresent());
            paramNames[i] = parameters[i].getName();
        }
        Object[] paramValues = pjp.getArgs();
        return SpelParser.getKey(key, paramNames, paramValues);
    }

    /**
     * 记录错误日志
     *
     * @Param id           业务id
     * @Param className    Service类名
     * @Param method       切面拦截的方法名
     * @Param errorInfo    错误信息
     * @Return void
     */
    private void errorLog(Object id, String className, String method, String errorInfo) {
        System.err.println("[ERROR] id=" + id + ", className=" + className + ", method=" + method + ", errorInfo=" + errorInfo);
    }

    /**
     * 记录正常日志
     *
     * @Param id           业务id
     * @Param className    Service类名
     * @Param method       切面拦截的方法名
     * @param returnInfo   响应结果
     * @Return void
     */
    private void infoLog(Object id, String className, String method, String returnInfo) {
        System.out.println("[INFO] id=" + id + ", className=" + className + ", method=" + method + ", returnInfo=" + returnInfo);
    }
}

SpEL解析类:

/**
 * SpEL解析类
 *
 * @author wanglingqiang
 * @date 2020/2/1 下午4:32
 **/
public class SpelParser {

    private static ExpressionParser parser = new SpelExpressionParser();

    /**
     * 获取占位符对应的实际值
     *
     * @Param key          占位符,如#id
     * @Param paramNames   形参名称
     * @Param args         参数值
     * @Return java.lang.String
     **/
    public static String getKey(String key, String[] paramNames, Object[] args) {
        //将key字符串解析成spel表达式
        Expression exp = parser.parseExpression(key);

        //初始化上下文
        EvaluationContext context = new StandardEvaluationContext();
        if (args.length <= 0) {
            return null;
        }

        //将形参参数名和形参参数值配对并赋值到上下文中
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }

        //根据赋值上下文运算spel表达式
        return exp.getValue(context, String.class);
    }

    public static void main(String[] args) {
        String key = "#id+' '+#name"; //el表达式字符串
        String[] paramNames = new String[]{"id", "name"};//参数名称
        Object[] paramValues = new Object[]{1, "wlq"};

        System.out.println(SpelParser.getKey(key, paramNames, paramValues));
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值