手写一个博客平台 ~ 第五天

作者: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容器启动的生命周期执行流程:

服务器加载Servlet
Servlet构造函数
PostConstruct
Init
Service
Destroy
PreDestroy
服务器卸载Servlet完毕

加上注解@Autowire后,是这样的一个流程

服务器加载Servlet
Servlet构造函数
@Autowire
@PostConstruct

所以可使用@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是第二道校验墙
    • 是没法破解,而且部署代理服务的服务器,切忌放数据存储的MySQLRedis,这样就没法破译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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嗝屁小孩纸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值