注解
何谓注解?
在Java中,注解(Annotation)是一种特殊的语法,用@符号开头,是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
拿熟悉 的@Override 注解来看。
package java.lang;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
JDK 内置了很多注解(比如 @Override、@Deprecated),其他框架如 Spring 也内置了不少注解,我们也可以自定义注解。
注解的作用
注解的主要作用是提供元数据,具体可以用于以下几个方面:
- 编译时检查:如@Override可以帮助编译器检查该方法是否正确重写了父类的方法。
- 代码生成:如@Entity可以告诉框架生成对应的数据库表。
- 运行时处理:如@Deprecated可以在运行时提醒开发者某个方法或类已经不建议使用。
注解的解析方法有哪几种?
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value、@Component)都是通过反射来进行处理的。也是我们自定义注解中使用最多的。
如何自定义注解?
自定义注解主要包括以下几个步骤:
- 1.定义注解:使用@interface关键字定义注解。
- 2.注解元素:在注解中定义元素,就像在接口中定义方法。
- 3.元注解:使用元注解(如@Retention、@Target等)来描述注解的行为。
1. 定义注解
可以使用@interface关键字来定义一个注解。下面是一个简单的例子:
public @interface MyAnnotation {
String value();
int number() default 0;
}
在上面的例子中, MyAnnotation 注解有两个元素: value 和 number 。其中, number 有一个默认值 0 。
2. 元注解
元注解是注解的注解,用来描述注解本身的行为。常见的元注解有:
- @Retention:指明注解的保留策略。
- @Target:指明注解的使用目标。
@Retention
@Retention指定了注解的生命周期,它有三个取值:
- RetentionPolicy.SOURCE:注解只在源代码中存在,编译后就不存在了。
- RetentionPolicy.CLASS:注解在编译后会存在于.class文件中,但在运行时不会存在。
- RetentionPolicy.RUNTIME:注解在运行时依然存在,可以通过反射读取。
@Target
@Target指定了注解可以使用的地方,如类、方法、字段等。常见的取值有:
- ElementType.TYPE:用于类、接口、枚举、注解类型。
- ElementType.FIELD:用于字段或属性。
- ElementType.METHOD:用于方法。
- ElementType.PARAMETER:用于参数。
- ElementType.CONSTRUCTOR:用于构造函数。
- ElementType.LOCAL_VARIABLE:用于局部变量。
3. 完整的自定义注解示例
下面是一个包含@Retention和@Target元注解的完整自定义注解示例:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
// 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value();
int number() default 0;
}
4. 使用自定义注解
定义完注解后,可以在代码中使用它:
public class Test {@MyAnnotation(value = "Test method", number = 42)
public void testMethod() {
// 方法的具体实现
}
}
5. 通过反射读取注解
可以使用反射机制读取并处理注解(本项目中的 AOP 切面原理就是如此):
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Method method = Test.class.getMethod("testMethod");if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
System.out.println("Value: " + annotation.value());
System.out.println("Number: " + annotation.number());
}
}
}
Spring 特性
八股中的 Spring 的两大特性熟的不能再熟了吧?
●IoC(控制反转)
●AOP 面向切面编程
面向切面编程 AOP
这里我们重点介绍下 AOP,因为项目中使用到。
面向切面编程是一种编程范式,它允许在不改变业务逻辑代码的情况下,将横切关注点(如日志记录、事务管理、安全检查等)模块化。AOP通过定义切面(Aspect)和切点(Pointcut)来实现这一点。
Spring AOP 提供了多种方式来定义和使用切面,包括:
●注解:使用@Aspect和相关注解(如@Before、@After、@Around等)来定义切面和切点。
●XML配置:在Spring配置文件中定义切面和切点(较少使用,现代开发中更常用注解)。
微服务架构
微服务简而言之就是单个独立的服务,可以独立开发、部署和维护。而微服务架构是指的多个微服务聚合起来的系统,这个系统涵盖多个微服务,服务与服务之间的通讯、服务监控、服务熔断降级、服务注册、分布式配置、分布式事务等各种解决方案聚合而成的架构体系。
微服务架构有如下优点:
- 提高开发效率:团队可以并行开发不同的微服务,减少了开发和发布的时间。
- 增强可维护性:小而专注的代码库更易于理解和维护,降低了技术债务。
- 灵活的技术选型:不同的微服务可以根据需要使用最合适的技术栈,而无需在整个系统中保持一致。
- 持续交付和部署:微服务架构支持持续集成和持续交付(CI/CD),使得新功能和修复能够快速上线。
- 更好的故障隔离:一个微服务的故障不会影响其他微服务的正常运行,从而提高系统的可靠性。
- 按需扩展:可以独立地扩展需要高负载处理的微服务,优化资源使用和成本。
鉴权基础
鉴权顾名思义就是需要进行权限认证和授权控制,你写好的系统不希望谁都可以访问吧?你写的牛逼的接口也不希望哪个人都可以来蹭一下访问吧?那就需要认证和授权。
专业做这块的有 Spring Security 和 Shiro 这两哥们,当然还有一些其他的框架也是可以做的,但无非核心都在做两件事:
- 认证
- 授权
认证,说白了就是登录,传统 web 登录是通过用户名和密码用 Cookie+Session 的方式,这种依赖于服务器本地内存,微服务中,显然不合适。
常见的鉴权方式有以下几种:
用户名和密码
是最传统和常见的鉴权方式,用户通过输入预先设置的用户名和密码进行登录,需要注意密码的存储和传输安全,如使用加盐哈希存储和HTTPS 传输。
多因素认证(MFA)
这是一种增强安全性的方法,通过要求多种不同类型的验证因素来确认用户身份,常见的因素包括:知识因子(密码)、拥有因子(手机验证码)、生物因子(指纹、面部识别)。
OAuth(开放授权)
这是一种一种授权协议,允许第三方应用以有限的权限访问用户资源,而无需暴露用户的凭证。常用于社交登录和API访问控制。
JWT(JSON Web Token)
一种基于 JSON 的开放标准(RFC 7519),用于在各方之间传递声明。JWT包含用户信息和签名,可用于鉴权和授权。我们这次也是采用的这种方式进行的鉴权。
项目实战中如何做鉴权认证
项目中的架构
微服务架构中,通常有多个独立服务组成,这些服务可能部署在不同的服务器或数据中心, 鉴权机制需要在分布式环境中有效运作,确保各个服务能安全通信,且需要有统一认证中心,我们先来看一张 PmHub 的架构图:
PmHub 中有一个单独的微服务来做认证,也就是认证服务 pmhub-auth,对于 PmHub 而言,请求一般分为 2 种:
- 通过 API 网关的请求
- 微服务内部请求
对于这两种请求,都需要进行鉴权,但方式是不一样的
PmHub 中如何做认证
微服务中的认证最多的方式是通过 JWT 令牌的方式,但 JWT 实际上是无状态的,也就是没法确定登录的用户啥时候过期,所以大部分情况下会需要结合 Redis 来设置状态。

将生成的 JWT 字符串在 Redis 上也保存一份,并设置过期时间,判断用户是否登录时,需要先去 Redis 上查看 JWT 字符串是否存在,存在的话再对 JWT 字符串做解析操作,如果能成功解析,就没问题,如果不能成功解析,就说明令牌不合法。

PmHub 也是采取的这个逻辑,这是一个简单的流程图:
在认证服务中,检查用户名密码的正确性,正确的话就生成 JWT 字符串,同时再把数据存入到 Redis 上,然后返回 token 信息。登录请求先经过网关,网关再转发到认证服务,下面是一个具体的流程:
放在系统层面上流程用例比较复杂,为了方便大家理解,可以看如下图:
可以看到用户登录逻辑其实是涉及到多服务交互的,大家可以对着代码看流程图,理解起来会更深入一些。
PmHub 中如何做鉴权
鉴权(或者说是授权)是请求到达每个微服务后,需要对请求进行权限判定,看是否有权限访问,通常不会放在网关中来做。还是在微服务中自己来做。
我们说过,在 PmHub 中,请求主要分为 2 种,外部请求和内部请求,下面是不同的授权思路。
外部请求
PmHub 的做法是:请求到达网关后,通过微服务的自定义请求头拦截器(可以放在公共包下面,每个服务都可以引用),配合自定义注解和 AOP,拦截请求头,获取用户和权限信息,然后进行比对,有权限则放行,没权限则抛出异常。
内部请求
对于内部的请求来说,正常是不需要鉴权的,内部请求可以直接处理。问题是如果使用了 OpenFeign,数据都是通过接口暴露出去的,不鉴权的话,又会担心从外部来的请求调用这个接口,对于这个问题,我们也可以自定义注解+AOP,然后在内部请求调用的时候,额外加一个头字段加以区分。
我采用的是自定义内部请求注解,然后 AOP 控制拦截。
内部请求注解
/*** 内部认证注解* * @author canghe*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InnerAuth
{
/*** 是否校验用户信息*/
boolean isUser() default false;
}
AOP 的切面控制请求是否携带有内部请求的标识:
内部请求切面
/*** 内部服务调用验证处理** @author canghe*/
@Aspect
@Component
public class InnerAuthAspect implements Ordered {@Around("@annotation(innerAuth)")
public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable {
String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);
// 内部请求验证
if (!StringUtils.equals(SecurityConstants.INNER, source)) {
throw new InnerAuthException("没有内部访问权限,不允许访问");
}String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);
String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
// 用户信息验证
if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))) {
throw new InnerAuthException("没有设置用户信息,不允许访问 ");
}
return point.proceed();
}
因为使用的是 OpenFeign,请求通过 OpenFeign 调用也需要鉴权,所以我实现了 feign.RequestInterceptor 接口来定义一个 OpenFeign 的请求拦截器,在拦截器中,统一为 OpenFeign 请求设置请求头信息。
/*** feign 请求拦截器** @author canghe*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {@Override
public void apply(RequestTemplate requestTemplate) {
HttpServletRequest httpServletRequest = ServletUtils.getRequest();
if (StringUtils.isNotNull(httpServletRequest)) {
Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
// 传递用户信息请求头,防止丢失
String userId = headers.get(SecurityConstants.DETAILS_USER_ID);
if (StringUtils.isNotEmpty(userId)) {
requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);
}
String userKey = headers.get(SecurityConstants.USER_KEY);
if (StringUtils.isNotEmpty(userKey)) {
requestTemplate.header(SecurityConstants.USER_KEY, userKey);
}
String userName = headers.get(SecurityConstants.DETAILS_USERNAME);
if (StringUtils.isNotEmpty(userName)) {
requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);
}
String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
if (StringUtils.isNotEmpty(authentication)) {
requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
}// 配置客户端IP
requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr());
}
}
以上,是 PmHub 中的认证鉴权逻辑。