作者:fyupeng
技术专栏:☞ https://github.com/fyupeng
项目地址:☞ https://github.com/fyupeng/distributed-blog-system-api
项目预览地址:☞ 博客第五天
留给读者
有人肯定会抱怨,为什么前一天的博客中只给了服务分离和springboot
不占用端口方式启动,其实我想找找一种最简便的方法,因为我使用了RPC
方式去创建的对象,已经违反了Spring
的容器管理,所以就算你反射创建成功了容器,比如Service
你创建了实例,但里面的成员变量UserMapper
反射并不会帮你一并创建,这时直接在成员变量上注解@Autowire
是不起作用的
现在来讲一下原因
反射原理创建对象,是在构造器函数执行完后就立马将该对象返回,而注解@Autowire
是在构造器执行完之后再自动注入的。
所以想通过反射也只能就是通过构造函数,是绝对不能获取到成员变量,因为它永远为null
,但是反过来想,在反射创建后的对象时,等@Autowire
依赖注入之后,再把对象返回,这时才去操作Mapper
,是不是就可以了呢。
从JavaEE5
开始引入了下面两个影响容器生命周期的注解:
@PostConstruct // 构造器执行后
@PreDestroy // 容器真正销毁前,并不是在Destroy之前
那么这时候我们可以看一看Java
容器启动的生命周期执行流程:
加上注解@Autowire
后,是这样的一个流程
所以可使用@PostCOnstruct
注解待@Autowire
注入完毕后返回
贴上完整代码
- 应用接口
@ApiOperation(value = "testRpcMysql", notes = "测试rpc协议数据库的接口")
@PostMapping(value = "/testRpcMysql")
public BlogJSONResult testRpcMysql() {
User user = new User();
user.setId("zszszs");
user.setUsername("zs");
user.setPassword("zspassword");
user.setPermission(1);
TestMySqlService proxy = rpcClientProxy.getProxy(TestMySqlService.class);
int result = proxy.testMySql(user);
return BlogJSONResult.ok("执行结果: " + result);
}
- 代理服务接口
package com.fyupeng.service;
import com.fyupeng.pojo.User;
/**
* @Auther: fyp
* @Date: 2022/8/16
* @Description:
* @Package: com.fyupeng.controller
* @Version: 1.0
*/
public interface TestMySqlService {
int testMySql(User user);
}
- 真实服务实现
记住,通过反射创建的对象,一定要有无参构造方法
package com.fyupeng.service.impl;
import com.fyupeng.mapper.UserMapper;
import com.fyupeng.pojo.User;
import com.fyupeng.service.TestMySqlService;
import com.zhkucst.anotion.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @Auther: fyp
* @Date: 2022/8/16
* @Description:
* @Package: com.fyupeng.service.impl
* @Version: 1.0
*/
@Service
@Component
public class TestMySqlServiceImpl implements TestMySqlService {
public static TestMySqlServiceImpl testMySqlServiceImpl;
@Autowired
private UserMapper userMapper;
//通过反射创建的对象,一定要有无参构造方法
public TestMySqlServiceImpl() {
}
// 这里会在反射调用 无参构造方法后执行
@PostConstruct
public void init() {
testMySqlServiceImpl = this;
}
@Override
public int testMySql(User user) {
return testMySqlServiceImpl.userMapper.insert(user);
}
}
已经跟上来的小伙伴们,有没有已经实现了一个应用接口请求了多台真实服务的接口呢?有没有实现了一个分布式微服务的雏形了呢?
当然在这里,我一直都在完善我的RPC
框架,有问题会一直更新维护,当然大家可以一起参与开源,作者原意与有对RPC
一定的理解而且热爱开源的伙伴们一起开发!
现在先对前面的代码进行些小优化,降低代码耦合度,做法很简单,就是尽可能将多变的变量抽离出来,通过配置文件注入。
使用到了springboot
提供的@ConfigurationProperties
和@PropertySource
两个注解。
1. 优化启动器
1.1 资源配置
- 编写配置文件
resource.properties
com.fyupeng.registerCenterServerPort=8082
- 获取配置文件信息到
ResourceConfig
类中
package com.fyupeng.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* @Auther: fyp
* @Date: 2022/8/17
* @Description: 资源配置
* @Package: com.fyupeng.config
* @Version: 1.0
*/
@Configuration
@ConfigurationProperties(prefix="com.fyupeng")
//不使用默认配置文件application.properties和application.yml
@PropertySource("classpath:resource.properties")
public class ResourceConfig {
private int registerCenterServerPort;
public int getRegisterCenterServerPort() {
return registerCenterServerPort;
}
public void setRegisterCenterServerPort(int registerCenterServerPort) {
this.registerCenterServerPort = registerCenterServerPort;
}
}
1.2 启动器优化
package com.fyupeng;
import com.fyupeng.config.ResourceConfig;
import com.zhkucst.anotion.ServiceScan;
import com.zhkucst.enums.SerializerCode;
import com.zhkucst.net.netty.server.NettyServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import tk.mybatis.spring.annotation.MapperScan;
/**
* @Auther: fyp
* @Date: 2022/8/13
* @Description:
* @Package: com.fyupeng
* @Version: 1.0
*/
@Slf4j
@ServiceScan
@SpringBootApplication
@EnableScheduling
@MapperScan(basePackages = "com.fyupeng.mapper")
@ComponentScan(basePackages = {"com.fyupeng", "org.n3r.idworker"})
public class UserServer implements CommandLineRunner {
@Autowired
private ResourceConfig resourceConfig;
public static void main(String[] args) throws InterruptedException {
SpringApplication.run(UserServer.class, args);
}
@Override
public void run(String... args) throws Exception {
//这里也可以添加一些业务处理方法,比如一些初始化参数等
while(true){
NettyServer nettyServer = new NettyServer("127.0.0.1", resourceConfig.getRegisterCenterServerPort(), SerializerCode.KRYO.getCode());
log.info("Service bind in port with "+ resourceConfig.getRegisterCenterServerPort() +" and start successfully!");
nettyServer.start();
log.error("RegisterAndLoginService is died,Service is restarting....");
}
}
}
当然上面的序列化方式也可以从配置文件获取
客户端的话,也就是代理服务的Controller
类,我是直接使用基类来写固定了,其实可以动态来注入,这里只说怎么实现。
首先需要使用到Enum
单例,可以简单做一个映射,在做选择策略模式时,比如字符串"random"
指定为随机策略,"roundRobin"
为轮询策略,每个单例设置两个成员变量,一个表示策略类LoadBalancer
,另一个是String
类型的loadValue
,可以自定义一个方法来getBlance
,参数为String
类型,根据前面去匹配具体的策略类,然后自己new创建对应的策略返回即可,所以自己要定义这么个协议,然后写到resource.properties
中,获取对应的策略,从Enum
中选择后返回示例对象即可。
示例:NettyServer nettyServer = new NettyServer("127.0.0.1", 5000, SerializerCode.KRYO.getCode());
具体的策略类到方法内部实现即可,这是一种设计模式(最少知识原则),使用者完全不用考虑怎么使用策略类,而是转而提供一种Enum
单例来间接选择策略。
可以参照模仿我在框架RPC
中的实现
设计Enum
类可以 增强可读性,从而不需要阅读源码即可调用使用
public enum SerializerCode {
KRYO(0),// KRYO 序列化 方式
JSON(1); // JSON 序列化方式
private final int code;
SerializerCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
策略接口模仿实现对应的获取
public interface CommonSerializer {
Integer KRYO_SERIALIZER = 0;
Integer JSON_SERIALIZER = 1;
Integer DEFAULT_SERIALIZER = KRYO_SERIALIZER;
byte[] serialize(Object obj);
Object deserialize(byte[] bytes, Class<?> clazz);
int getCode();
static CommonSerializer getByCode(int code) {
switch (code) {
case 0:
return new KryoSerializer();
case 1:
return new JsonSerializer();
default:
return null;
}
}
}
我这里没有实现这种做法,而是使用公用基类,由其他具体Controller
实现,因为在Nacos
服务中,是可以在控制台线上控制负载的。
package com.fyupeng.controller;
import com.zhkucst.loadbalancer.RoundRobinLoadBalancer;
import com.zhkucst.net.netty.client.NettyClient;
import com.zhkucst.proxy.RpcClientProxy;
import com.zhkucst.serializer.CommonSerializer;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BasicController {
private static final RoundRobinLoadBalancer roundRobinLoadBalancer = new RoundRobinLoadBalancer();
private static final NettyClient nettyClient = new NettyClient(roundRobinLoadBalancer, CommonSerializer.KRYO_SERIALIZER);
protected RpcClientProxy rpcClientProxy = new RpcClientProxy(nettyClient);
}
测试的具体Controller
类
package com.fyupeng.controller;
import com.fyupeng.pojo.User;
import com.fyupeng.service.HelloService;
import com.fyupeng.service.TestMySqlService;
import com.fyupeng.utils.BlogJSONResult;
import com.zhkucst.loadbalancer.RoundRobinLoadBalancer;
import com.zhkucst.net.netty.client.NettyClient;
import com.zhkucst.proxy.RpcClientProxy;
import com.zhkucst.serializer.CommonSerializer;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SuppressWarnings("all")
@RestController
@RequestMapping(value = "/HelloWorld")
@Api(value = "第一个HelloWorld接口", tags = {"第一个HelloWorld接口的controller"})
public class HelloWorldController extends BasicController{
//private static final RoundRobinLoadBalancer roundRobinLoadBalancer = new RoundRobinLoadBalancer();
//private static final NettyClient nettyClient = new NettyClient(roundRobinLoadBalancer, CommonSerializer.KRYO_SERIALIZER);
//
//private RpcClientProxy rpcClientProxy = new RpcClientProxy(nettyClient);
@ApiOperation(value = "helloWorld", notes = "helloWorld的接口")
@GetMapping(value = "/helloWorld")
@Deprecated
public BlogJSONResult helloWorld() {
return BlogJSONResult.ok();
}
@ApiOperation(value = "testMysql", notes = "测试数据库的接口")
@PostMapping(value = "/testMysql")
@Deprecated
public BlogJSONResult testMysql() {
return BlogJSONResult.ok();
}
@ApiOperation(value = "testRpc", notes = "测试Rpc微服务的接口")
@PostMapping(value = "/testRpc")
public BlogJSONResult testRpc(String message) {
HelloService proxy = rpcClientProxy.getProxy(HelloService.class);
BlogJSONResult result = proxy.sayHello(message);
return result;
}
@ApiOperation(value = "testRpcMysql", notes = "测试rpc协议数据库的接口")
@PostMapping(value = "/testRpcMysql")
public BlogJSONResult testRpcMysql() {
User user = new User();
user.setId("zsId");
user.setUsername("zs");
user.setPassword("zsPassword");
user.setPermission(1);
TestMySqlService proxy = rpcClientProxy.getProxy(TestMySqlService.class);
int result = proxy.testMySql(user);
return BlogJSONResult.ok("执行结果: " + result);
}
}
这样优化鸡汤算是结束了,下面给一块肥肉,眼红别噎着
使用 JWT鉴权
+ 自定义注解
+ Redis
+ 拦截器
实现一个完整用户权限控制的调用API
流程
链接:☞ http://www.45fan.com/article.php?aid=1D5epLrIa71lm7sN
说白就是以下几点:
-
前端调用后端接口登录,后端分发给用户秘钥,该秘钥是作为每个用户的标识
- 秘钥包含了用户
ID
、用户名和密码(MD5
加密) - 每个秘钥都有存活时间,失效将被拦截器拦截,无法访问接口
- 每个秘钥都有自己的标志,也就是自己的签名,注意保密
- 秘钥包含了用户
-
前端获取登录接口返回的秘钥token后,保存到浏览器中
-
前端每次访问其他
API
接口,都会校验请求头中的token
是否过期JWT
鉴权校验成功,会去校验Redis
的秘钥token
是否过期Redis
保存的秘钥主要用于用户主动注销(主动失效)JWT
不能做到主动失效,故而结合两者,成为双重校验秘钥- 就算成功破解了秘钥(泄露了秘钥的签名),企图延长时间,但
Redis
是第二道校验墙 - 是没法破解,而且部署代理服务的服务器,切忌放数据存储的
MySQL
和Redis
,这样就没法破译Redis
其实实现起来并不是很难,跟着我思路就对了
2. 定义拦截路径配置 InterceptorConfig
package com.fyupeng.controller.config;
import com.fyupeng.controller.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Auther: fyp
* @Date: 2022/8/18
* @Description: 拦截器
* @Package: com.fyupeng.controller.config
* @Version: 1.0
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
// 拦截 user 下的所有接口
@Override
public void addInterceptors(InterceptorRegistry registry) {
String[] addPathPatterns= {
"/user/**"
};
// 跳过拦截的接口
String[] excludePathPatterns={
"/user/login",
"/user/regist"
};
registry.addInterceptor(loginInterceptor).addPathPatterns(addPathPatterns).excludePathPatterns(excludePathPatterns);
}
}
3. 配置自定义注解
不拦截接口
package com.fyupeng.controller.annotion;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Auther: fyp
* @Date: 2022/8/18
* @Description: 通过Token注解
* @Package: com.fyupeng.controller.annotion
* @Version: 1.0
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
拦截接口
package com.fyupeng.controller.annotion;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Auther: fyp
* @Date: 2022/8/18
* @Description: 用户登录Token注解
* @Package: com.fyupeng.controller.annotion
* @Version: 1.0
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
目前还没有做到注解到类上的使用,虽然有注明该注解ElementType.TYPE
只能使用到方法上,无恙,后面有需求再做补充
4. 配置拦截器
package com.fyupeng.controller.interceptor;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.fasterxml.jackson.databind.introspect.TypeResolutionContext;
import com.fyupeng.controller.BasicController;
import com.fyupeng.controller.annotion.PassToken;
import com.fyupeng.controller.annotion.UserLoginToken;
import com.fyupeng.pojo.User;
import com.fyupeng.service.UserService;
import com.fyupeng.utils.RedisUtils;
import com.fyupeng.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* @Auther: fyp
* @Date: 2022/8/18
* @Description:
* @Package: com.fyupeng.controller.interceptor
* @Version: 1.0
*/
@Component
public class LoginInterceptor extends BasicController implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("prehandle");
String token = request.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//检查方法是否有passtoken注解,有则跳过认证,直接通过
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
String userName;
String password;
try {
userId = JWT.decode(token).getClaim("userId").asString();
userName = JWT.decode(token).getClaim("username").asString();
password = JWT.decode(token).getClaim("password").asString();
} catch (JWTDecodeException j) {
throw new RuntimeException("token不正确,请不要通过非法手段创建token");
}
//查询数据库,看看是否存在此用户,方法要自己写
UserService userServiceProxy = rpcClientProxy.getProxy(UserService.class);
// password 为 MD5 加密密文
User user = userServiceProxy.queryUserForLogin(userName, password);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
if (TokenUtils.verify(token)) {
String userRedisSession = RedisUtils.getUserRedisSession(userId);
if(redis.get(userRedisSession) != null)
return true;
return false;
} else {
throw new RuntimeException("token过期或不正确,请重新登录");
}
}
}
throw new RuntimeException("没有权限注解一律不通过");
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("post5handle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("aftercmpletion");
}
}
5. 配置JWT鉴权
5.1 依赖
<!--JWT 校验 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
5.2 秘钥生成和校验工具
package com.fyupeng.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Auther: fyp
* @Date: 2022/8/18
* @Description:
* @Package: com.fyupeng.utils
* @Version: 1.0
*/
public class TokenUtils {
//设置过期时间
private static final long EXPIRE_DATE=1000*60*5; //5 分钟
//token秘钥,使用 CSDN 浏览器助手 开发工具箱 随机生成的
private static final String TOKEN_SECRET = "nB5iD0aA0bJ6jF4eC6oD1aI2cE1nD2mI0jB3";
public static String token (String userId, String username,String password) {
String token = "";
try {
//过期时间
Date date = new Date(System.currentTimeMillis()+EXPIRE_DATE);
//秘钥及加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
//设置头部信息
Map<String,Object> header = new HashMap<>();
header.put("typ","JWT");
header.put("alg","HS256");
//携带username,password信息,生成签名
token = JWT.create()
.withHeader(header)
.withClaim("userId",userId)
.withClaim("username",username)
.withClaim("password",password).withExpiresAt(date)
.sign(algorithm);
}catch (Exception e){
e.printStackTrace();
return null;
}
return token;
}
public static boolean verify(String token){
/**
* @desc 验证token,通过返回true
* @params [token]需要校验的串
**/
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
}catch (Exception e){
//System.out.println("校验失败");
return false;
}
}
}
最后一步,第二道防护墙
6. Redis秘钥保存
7. 配置 Redis
7.1 RedisUtils 工具
用于生成相关的key
值,唯一使用效果不佳的是,利用了请求的IP
地址为将来做阅读量统计,这里一个运营商SIM
卡只能做一个用户使用,而不根据不同终端或者不同用户登录来统计阅读量,或许也挺好的,看个人需求。
因为不能说登录后才能增加阅读量浏览,非用户登录应该也能够阅读文章,所以可以借此来做站外阅读和站内用户阅读,本章重点不在这里,有需要的可以自己琢磨实现,或者联系作者
package com.fyupeng.utils;
import javax.servlet.http.HttpServletRequest;
/**
* @Auther: fyp
* @Date: 2022/4/13
* @Description:
* @Package: com.crop.utils
* @Version: 1.0
*/
public class RedisUtils {
public static final String Blog = "blog";
public static final String USER_REDIS_SESSION = Blog + ":" + "user-redis-session";
public static final String ADMIN_REDIS_SESSION = Blog + ":" + "admin-redis-session";
public static final String IS_VIEW = Blog + ":" + "isView";
public static final String VIEW_COUNT = Blog + ":" + "view-count";
public static final String SEARCH_HISTORY = Blog + ":" + "search-history";
public static final String SEARCH_SCORE = Blog + ":" + "search-score";
public static String getUserRedisSession(String userId) {
return USER_REDIS_SESSION + ":" + userId;
}
public static String getAdminRedisSession(String userId) {
return ADMIN_REDIS_SESSION + ":" + userId;
}
public static String getIdView(String id, HttpServletRequest request) {
return IS_VIEW + ":" + id + ":" + RequestAddr.getClientIpAddress(request);
}
public static String getViewCount() {
return VIEW_COUNT + "*";
}
public static String getIdViewCount(String id) {
return VIEW_COUNT + ":" + id;
}
public static String getSearchHistoryKey(String userId) {
return SEARCH_HISTORY + ":" + userId;
}
public static String getSearchHistoryKeyWithSearchKey(String userId, String key) {
return SEARCH_HISTORY + ":" + userId + ":" + key;
}
public static String getSearchScoreKey() {
return SEARCH_SCORE;
}
public static String getSearchScoreKeyWithSearchKey(String key) {
return SEARCH_SCORE + ":" + key;
}
static class RequestAddr {
/**
* 获取客户端ip地址(可以穿透代理)
*
* @param request
* @return
*/
public static String getRemoteAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
private static final String[] HEADERS_TO_TRY = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR",
"X-Real-IP"};
/***
* 获取客户端ip地址(可以穿透代理)
* @param request
* @return
*/
public static String getClientIpAddress(HttpServletRequest request) {
for (String header : HEADERS_TO_TRY) {
String ip = request.getHeader(header);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
}
return request.getRemoteAddr();
}
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Real-IP");
if (null != ip && !"".equals(ip.trim())
&& !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
ip = request.getHeader("X-Forwarded-For");
if (null != ip && !"".equals(ip.trim())
&& !"unknown".equalsIgnoreCase(ip)) {
// get first ip from proxy ip
int index = ip.indexOf(',');
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
}
return request.getRemoteAddr();
}
}
}
为了尽可能照顾基础差些的同学,这里再提供一个Redis
数据库操作类,后续实现异步阅读量统计用到
什么异步?也就是开启子线程去做阅读量统计这件事,主线程不受任何影响,听起来挺有趣!
能坚持下去?坚持下去你肯定会有很大的收获!相信作者和相信自己!
7.2 RedisOperator 工具
package com.fyupeng.utils;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* @Description: 使用redisTemplate的操作实现类
*/
@Component
public class RedisOperator {
// @Autowired
// private RedisTemplate<String, Object> redisTemplate;
@Autowired
private StringRedisTemplate redisTemplate;
// Key(键),简单的key-value操作
/**
* 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
*
* @param key
* @return
*/
public long ttl(String key) {
return redisTemplate.getExpire(key);
}
/**
* 实现命令:expire 设置过期时间,单位秒
*
* @param key
* @return
*/
public void expire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:INCR key,增加key一次
*
* @param key
* @return
*/
public long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
*/
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 实现命令:DEL key,删除一个key
*
* @param key
*/
public void del(String key) {
redisTemplate.delete(key);
}
// String(字符串)
/**
* 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
*
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
*
* @param key
* @param value
* @param timeout
* (以秒为单位)
*/
public void set(String key, String value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:GET key,返回 key所关联的字符串值。
*
* @param key
* @return value
*/
public String get(String key) {
return (String)redisTemplate.opsForValue().get(key);
}
// Hash(哈希表)
/**
* 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
*
* @param key
* @param field
* @param value
*/
public void hset(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value);
}
/**
* 实现命令:HGET key field,返回哈希表 key中给定域 field的值
*
* @param key
* @param field
* @return
*/
public String hget(String key, String field) {
return (String) redisTemplate.opsForHash().get(key, field);
}
/**
* 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
*
* @param key
* @param fields
*/
public void hdel(String key, Object... fields) {
redisTemplate.opsForHash().delete(key, fields);
}
/**
* 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
*
* @param key
* @return
*/
public Map<Object, Object> hgetall(String key) {
return redisTemplate.opsForHash().entries(key);
}
// List(列表)
/**
* 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long lpush(String key, String value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* 实现命令:LPOP key,移除并返回列表 key的头元素。
*
* @param key
* @return 列表key的头元素。
*/
public String lpop(String key) {
return (String)redisTemplate.opsForList().leftPop(key);
}
/**
* 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long rpush(String key, String value) {
return redisTemplate.opsForList().rightPush(key, value);
}
}
学了这么多年,我是真的把自己该有的干货毫不保留给了你们,你们可以毫不吝啬给了关注作者嘛?
有没有无关紧要,肯定会有一些同学会跟着我走下去,继续加油吧,干它个分布式微服务、高并发、高可用的博客系统!
顺便给分享自己几年来做的技术专栏:https://github.com/fyupeng