• Spring Boot 总结


    Spring Boot 优点

    快速创建与框架集成
    
    内嵌Servlet容器
    
    starters自动依赖与版本控制
    
    自动配置
    
    运行时应用监控
    

    Spring Boot 自动配置

    @SpringBootApplication ->
    
    	@EnableAutoConfiguration ->
    
    		@AutoConfigurationPackage ->
    			
    			@Import({Registrar.class}):
    				扫描启动类所在的包及其子包的注解。
    
    		@Import({AutoConfigurationImportSelector.class}):
    			从类路径下的 META-INF/spring.factories 导入自动配置类
    			SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
    
    所有配置信息所在jar包:
    
    	spring-boot-autoconfigure-2.2.4.RELEASE.jar
    
    
    自动配置:
    
    	Spring Boot启动会加载大量的自动配置类(xxxAutoConfiguration)。
    	给容器中自动配置类添加组件的时候,会从Properties类(xxxProperties)中获取属性。
    
    	xxxAutoConfiguration:自动配置类
    
    	xxxProperties:封装配置文件中的相关属性
    
    	package org.springframework.boot.autoconfigure.thymeleaf;
    
    	@Configuration(proxyBeanMethods = false)
    	@EnableConfigurationProperties({ThymeleafProperties.class})
    	@ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class})
    	@AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class})
    	public class ThymeleafAutoConfiguration {}
    
    	package org.springframework.boot.autoconfigure.thymeleaf;
    
    	@ConfigurationProperties(prefix = "spring.thymeleaf")
    	public class ThymeleafProperties {
    	    private static final Charset DEFAULT_ENCODING;
    	    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    	    public static final String DEFAULT_SUFFIX = ".html";
    	    private boolean checkTemplate = true;
    	    private boolean checkTemplateLocation = true;
    	    private String prefix = "classpath:/templates/";
    	    private String suffix = ".html";
    	    private String mode = "HTML";
    	    private Charset encoding;
    	    private boolean cache;
    	    private Integer templateResolverOrder;
    	    private String[] viewNames;
    	    private String[] excludedViewNames;
    	    private boolean enableSpringElCompiler;
    	    private boolean renderHiddenMarkersBeforeCheckboxes;
    	    private boolean enabled;
    	    private final ThymeleafProperties.Servlet servlet;
    	    private final ThymeleafProperties.Reactive reactive;
    
    	    public ThymeleafProperties() {
    	        this.encoding = DEFAULT_ENCODING;
    	        this.cache = true;
    	        this.renderHiddenMarkersBeforeCheckboxes = false;
    	        this.enabled = true;
    	        this.servlet = new ThymeleafProperties.Servlet();
    	        this.reactive = new ThymeleafProperties.Reactive();
    	    }
    	}
    
    
    @EnableWebMvc:完全接管SpringMvc配置,取消SpringBoot自动配置。
    

    Spring Boot 国际化配置

    spring.messages.basename=message.login(message是classpath下的文件夹,login是国际化配置文件名)

    Spring Boot 视图解析器配置

    WebMvcConfigurerAdapter -> registry.addViewController("/").setViewName("index");

    Spring Boot 指定日期格式

    spring.mvc.date-format=yyyy-MM-dd HH:mm

    Spring Boot 拦截器配置

    public class LoginInterceptor extends HandlerInterceptorAdapter {
    
        @Override
        public boolean preHandle(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler) throws Exception {
            if(request.getSession().getAttribute("user") == null){
                response.sendRedirect("/admin/user");
                return false;
            }
            return true;
        }
    }
    
    
    SpringBoot配置拦截器路径(SpringBoot已经配置了静态资源映射):
    
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())
                    .addPathPatterns("/admin/**")
                    .excludePathPatterns("/admin/user/login");
        }
    

    Spring Boot 获取配置文件的值

    @Component
    @PropertySource(value = {"classpath: person.properties"})	//加载指定配置文件
    @ConfigurationProperties(prefix = "person")
    	告诉Spring Boot此类中所有属性和配置文件中的相关配置进行绑定
    	支持JSR303
    
    @Configuration
    public class DruidConfig {
    	@ConfigurationProperties(prefix = "spring.datasource")
    	@Bean
    	public DataSource druid(){
    		return new DruidDataSource();
    	}
    }
    
    
    @Value("{person.name}")
    	支持SpEL
    

    Spring Boot 配置文件加载位置

    -file:./config/
    
    -file:./
    
    -classpath:/config/
    
    -classpath:/
    

    Spring Boot thymeleaf

    指定版本:
    
    	<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
    	<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
    
    
    只需将HTML页面放在classpath:/templates/,thymeleaf就能自动渲染(默认配置在classpath:/templates/)
    
    
    导入thymeleaf名称空间:xmlns:th="http://www.thymeleaf.org"
    
    
    表达式语法:
    
    	${}:
    		底层是ognl表达式
    		可以调用对象的属性和方法(${person.name},${list.size()})
    		使用内置的基本对象(${#locale.country})
    		使用内置的工具对象(${#strings.toString(obj)})
    
    	*{}:
    		和${}功能一样
    		补充属性:配合th:object="${person}"使用(*{name})
    
    	#{}:获取国际化内容(#{home.welcome})
    
    	@{}:替换url(@{/login(username=${person.name}, password=${person.password})})
    
    	~{}:片段引用表达式
    
    
    文本操作:
    	+:字符串拼接
    	| the name is ${person.name} |:字符串替换
    
    
    默认值:value ?: defaultvalue
    
    
    标签内表达式:
    	[[${}]] -> th:text
    	[(${})] -> th:utext
    
    
    抽取片段:
    
    	th:fragment="header(n)"
    
    	插入:
    		将公共片段整个插入到声明的标签中 -> th:insert="~{fragment/fragment::header(1)}"(相对于templates目录)
    	替换:
    		将声明的标签替换为公共片段 -> th:replace="~{fragment/fragment::header}"(相对于templates目录)
    	包含:
    		将公共片段的内容包含进声明的标签中 -> th:include="~{fragment/fragment::header}"(相对于templates目录)
    
    
    	也可以用id选择器(id="header"):
    
    		th:insert="~{fragment/fragment::#header}"
    

    thymeleaf 发送Put请求

    配置HiddenHttpMethodFilter:SpringBoot已自动配置好
    
    form表单(method="post")中创建一个input项:
    	<input type="hidden" name="_method" value="put" th:if="${person!=null}"/>
    
    @PutMapping("/person")
    

    thymeleaf 发送Delete请求

    配置HiddenHttpMethodFilter:SpringBoot已自动配置好
    
    <button calss="deteteBtn" th:attr="del_url=@{/person/}+${person.id}">删除</button>
    
    <form id="deleteForm" method="post">
    	<input type="hidden" name="_method" value="delete"/>
    </form>
    
    <script>
    	$(".deteteBtn").click(function(){
    		$("#deleteForm").attr("action",$(this).attr("del_url")).submit();
    	})
    </script>
    
    @DeleteMapping("/person/{id}")
    

    Spring Boot 定制错误页面

    页面能获取的信息:
    	timestamp
    	status
    	error
    	exception
    	message
    	errors(JSR303检验错误)
    
    1. 有模板引擎:在templates文件夹下的error文件夹下创建error.html
    
    2. 没有模板引擎:静态资源文件夹下的error文件夹下创建error.html
    
    
    定制错误的Json数据:
    
    	@ControllerAdvice
    	public class MyExceptionHandler {
    
    		@ExceptionHandler(NotFoundException.class)
    		public String handleException(Exception e, HttpServletRequest request){
    			Map<String,Object> map = new HashMap<>();
    			request.setAttribute("javax.servlet.error.status_code",500);
    			map.put("code","500");
    			map.put("message",e.getMessage());
    			return "forward:/error";
    		}
    	}
    
    	/error请求会被BasicErrorController处理
    
    	//给容器中加入我们自己定义的ErrorAttributes
    	@Component
    	public class MyErrorAttributes extends DefaultErrorAttributes {
    		@Override
    		public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
    			Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
    			map.put("name","Sara");
    			return map;
    		}
    	}
    

    Spring Boot 定制和修改Servlet容器相关配置

    修改配置文件中以server开头的相关配置
    	server.port=8081
    	server.context‐path=/crud
    

    Spring Boot Mybatis使用

    @Mapper
    public interface PersonMapper {
    
    	@Select("select * from person where id=#{id}")
    	public Person getById(Integer id);
    
    	@Delete("delete from person where id=#{id}")
    	public int deleteById(Integer id);
    
    	@Options(useGeneratedKeys = true, keyProperty = "id")	//自动生成并封装主键
    	@Insert("insert into person(name) values(#{name})")
    	public int insert(Person person);
    
    	@Update("update person set name=#{name} where id=#{id}")
    	public int update(Person person);
    }
    
    使用MapperScan批量扫描所有的Mapper接口:
    	@MapperScan(value = "com.mapper")
    	@SpringBootApplication
    
    使用mapper配置文件:
    	mybatis:
    	  config‐location: classpath:mybatis/mybatis‐config.xml    #指定全局配置文件的位置
    	  mapper‐locations: classpath:mybatis/mapper/*.xml    #指定sql映射文件的位置*/
    

    Spring Boot 缓存

    CacheManager:
    	缓存管理器,管理各种缓存(Cache)组件。
    
    Cache:
    	缓存接口,定义缓存操作。
    	实现有:RedisCache、EhCacheCache、ConcurrentMapCache(默认缓存实现,数据保存在ConcurrentMap<Object, Object>中)等。
    
    
    @EnableCaching:
    	开启基于注解的缓存。
    
    
    @CacheConfig(value = "person"):
    	注解在类上,用于缓存公共配置。
    
    @Cacheable(value = {"缓存名:必须配置"}, key = "可以不配置"):
    	有数据时方法不会被调用。主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
    
    @CachePut(value = {"缓存名:必须配置"}, key = "可以不配置"):
    	保证方法总会被调用,并且缓存结果。主要针对缓存更新。
    
    @CacheEvict(value = {"缓存名:必须配置"}, key = "可以不配置", beforeInvocation = true):
    	清空缓存。
    
    
    keyGenerator:
    	缓存数据时key生成策略。
    
    serialize:
    	缓存数据时value序列化策略。
    
    
    整合Redis(对象必须序列化):
    
    	引入redis的starter后,容器中保存的是RedisCacheManager,默认CacheManager未注入。
    	RedisCacheManager会创建RedisCache作为缓存组件。
    

    Spring Boot 任务

    异步任务:
    	启动类上注解:@EnableAsync
    	方法是上注解:@Async
    
    	@Configuration
    	@EnableAsync
    	public class ThreadPoolTaskConfig {
    
    	    /**
    	     * 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,
    	     * 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
    	     * 当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝
    	     */
    
    	    /** 核心线程数(默认线程数) */
    	    private static final int corePoolSize = 20;
    	    /** 最大线程数 */
    	    private static final int maxPoolSize = 100;
    	    /** 允许线程空闲时间(单位:默认为秒) */
    	    private static final int keepAliveTime = 10;
    	    /** 缓冲队列大小 */
    	    private static final int queueCapacity = 200;
    	    /** 线程池名前缀 */
    	    private static final String threadNamePrefix = "Async-Service-";
    
    	    @Bean("taskExecutor")	//bean的名称,默认为首字母小写的方法名
    	    public ThreadPoolTaskExecutor taskExecutor(){
    	        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    	        executor.setCorePoolSize(corePoolSize);
    	        executor.setMaxPoolSize(maxPoolSize);
    	        executor.setQueueCapacity(queueCapacity);
    	        executor.setKeepAliveSeconds(keepAliveTime);
    	        executor.setThreadNamePrefix(threadNamePrefix);
    
    	        //线程池对拒绝任务的处理策略
    	        //CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
    	        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    	        // 初始化
    	        executor.initialize();
    	        return executor;
    	    }
    	}
    
    	@Async("taskExecutor")
    
    
    定时任务:
    	启动类上注解:@EnableScheduling
    	方法是上注解:@Scheduled(cron = "0 * * * * MON-SAT")	//周一到周五每分钟0秒启动一次
    
    
    邮件任务:
    	spring.mail.username=1622138424@qq.com
    	spring.mail.password=授权码
    	spring.main.host=smtp.qq.com
    	spring.mail.properties.mail.smtp.ssl.enable=true
    	使用JavaMailSenderImpl发送邮件
    

    Spring Boot Security

    编写配置类:
    	@EnableWebSecurity
    	继承WebSecurityConfigurerAdapter
    
    
    登陆/注销:
    	HttpSecurity配置登陆、注销功能
    
    Thymeleaf提供的SpringSecurity标签支持:
    	需要引入thymeleaf-extras-springsecurity4
    	sec:authentication="name"获得当前用户的用户名
    	sec:authorize="hasRole('ADMIN')"当前用户必须拥有ADMIN权限时才会显示标签内容
    
    remember me:
    	表单添加remember-me的checkbox
    	配置启用remember-me功能
    
    CSRF(Cross-site request forgery)跨站请求伪造:
    	HttpSecurity启用csrf功能,会为表单添加_csrf的值,提交携带来预防CSRF
    

    Spring Boot JWT(单点登录SSO)

    cookie:由服务器生成,发送给浏览器,浏览器把cookie以kv形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。
    
    session:服务器使用session把用户的信息临时保存在了服务器上,用户离开网站后session会被销毁。
    
    
    token应该在HTTP的头部发送从而保证了Http请求无状态。
    通过设置服务器属性Access-Control-Allow-Origin:*,让服务器能接受到来自所有域的请求。
    
    实现思路:
        用户登录校验,校验成功后就返回Token给客户端
        客户端收到数据后保存在客户端
        客户端每次访问API是携带Token到服务器端
        服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码
    
    
    JWT请求流程
    
        用户使用账号发出post请求
    
        服务器使用私钥创建一个jwt
    
        服务器返回这个jwt给浏览器
    
        浏览器将该jwt串在请求头中像服务器发送请求
    
        服务器验证该jwt
    
        返回响应的资源给浏览器
    
    JWT包含了三部分:
    
        Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
    
        Payload 负载(类似于飞机上承载的物品)
    
        Signature 签名/签证
    
    Header
    
    	JWT的头部承载两部分信息:token类型和采用的加密算法。
    
    	{
    	  "alg": "HS256",
    	  "typ": "JWT"
    	}
    
    Payload
    
    	载荷就是存放有效信息的地方。
    
    	有效信息包含三个部分
    
    	    标准中注册的声明
    
    	    公共的声明
    
    	    私有的声明
    
    	标准中注册的声明 (建议但不强制使用) :
    
    	    iss: jwt签发者
    
    	    sub: 面向的用户(jwt所面向的用户)
    
    	    aud: 接收jwt的一方
    
    	    exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
    
    	    nbf: 定义在什么时间之前,该jwt都是不可用的.
    
    	    iat: jwt的签发时间
    
    	    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
    
    	公共的声明:
    
    		公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
    
    	私有的声明:
    
    		私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
    
    Signature
    
    	jwt的第三部分是一个签证信息
    
    	这个部分需要base64加密后的header和base64加密后的payload使用'.'连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
    
    	密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。
    
    
    Spring Boot 和 JWT 的集成
    
    依赖
    
    	<dependency>
    	      <groupId>com.auth0</groupId>
    	      <artifactId>java-jwt</artifactId>
    	      <version>3.4.0</version>
    	</dependency>
    
    需要自定义两个注解
    
    	需要登录才能进行操作(UserLoginToken)
    
    		@Target({ElementType.METHOD, ElementType.TYPE})
    		@Retention(RetentionPolicy.RUNTIME)
    		public @interface UserLoginToken {
    		    boolean required() default true;
    		}
    
    用户Bean
    
    	@Data
    	public class User {
    	    String Id;
    	    String username;
    	    String password;
    	}
    
    需要写token的生成方法
    
    	public String getToken(User user) {
            String token="";
            token= JWT.create().withAudience(user.getId())
                    .sign(Algorithm.HMAC256(user.getPassword()));
            return token;
        }
    
    拦截器:
    
    	public class AuthenticationInterceptor implements HandlerInterceptor {
    	    
    	    @Autowired
    	    UserService userService;
    	    
    	    @Override
    	    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
    	        
    	        //如果不是映射到方法直接通过
    	        if(!(object instanceof HandlerMethod)){
    	            return true;
    	        }
    	        HandlerMethod handlerMethod = (HandlerMethod)object;
    	        Method method = handlerMethod.getMethod();
    
    	        //检查有没有需要用户权限的注解
    	        if (method.isAnnotationPresent(UserLoginToken.class)) {
    	            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
    	            if (userLoginToken.required()) {
    	                
    	                // 执行认证
    	                String token = httpServletRequest.getHeader("token");
    
    	                if (token == null) {
    	                    throw new RuntimeException("无token,请重新登录");
    	                }
    
    	                // 获取 token 中的 user id
    	                String userId;
    	                try {
    	                    userId = JWT.decode(token).getAudience().get(0);
    	                } catch (JWTDecodeException j) {
    	                    throw new RuntimeException("Token验证失败!");
    	                }
    	                User user = userService.findUserById(userId);
    	                if (user == null) {
    	                    throw new RuntimeException("用户不存在,请重新登录");
    	                }
    
    	                // 验证 token
    	                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
    	                try {
    	                    jwtVerifier.verify(token);
    	                } catch (JWTVerificationException e) {
    	                    throw new RuntimeException("用户密码错误!");
    	                }
    	            }
    	        }
    	        return true;
    	    }
    	}
    
    	@Configuration
    	public class InterceptorConfig implements WebMvcConfigurer {
    	    @Override
    	    public void addInterceptors(InterceptorRegistry registry) {
    	        registry.addInterceptor(new AuthenticationInterceptor())
    	                .addPathPatterns("/**");	//拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
    	    }
    	}
    
    Controller
    
    	@PostMapping("/login")
        public Result login(@RequestBody User user) {
            //处理验证,发送Token到前端
            ...
        }
    
    	@UserLoginToken
    	@GetMapping("/getMessage")
    	public Result getMessage() {
    		...
    	}
    

    SimpleDateFormat(线程不安全)

    单线程:private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    多线程:
    
    	private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
    
            @Override
            protected DateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            }
    
        };
    
        public static Date parse(String dateStr) throws ParseException {
            return threadLocal.get().parse(dateStr);
        }
    
        public static String format(Date date) {
            return threadLocal.get().format(date);
        }
    
    
        private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
        LocalDateTime date = LocalDateTime.now()
    
        public static String formatDate(LocalDateTime date) {
            return formatter.format(date);
        }
    
        public static LocalDateTime parseDate(String date) {
            return LocalDateTime.parse(date, formatter);
        }
    

    优化 Spring Boot

    server:
      tomcat:
        min-spare-threads: 20
        max-threads: 100
      connection-timeout: 5000
    
    java -Xms512m -Xmx768m -jar springboot-1.0.jar
    

    Redis + Token机制实现接口幂等性校验

    幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次
    
    为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis,
    请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
    
        1. 如果存在,正常处理业务逻辑,并从redis中删除此token,那么,如果是重复请求,由于token已被删除,则不能通过校验,返回请勿重复操作提示
        2. 如果不存在,说明参数不合法或者是重复请求,返回提示即可
    
    
    自定义注解@ApiIdempotent
    
    	/**
    	 * 在需要保证 接口幂等性 的Controller的方法上使用此注解
    	 */
    	@Target({ElementType.METHOD})
    	@Retention(RetentionPolicy.RUNTIME)
    	public @interface ApiIdempotent {}
    
    
    /**
     * 接口幂等性拦截器
     */
    public class ApiIdempotentInterceptor implements HandlerInterceptor {
    
        @Autowired
        private TokenService tokenService;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
    
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
    
            ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
            if (methodAnnotation != null) {
            	//幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
                check(request);
            }
    
            return true;
        }
    
        private void check(HttpServletRequest request) {
            tokenService.checkToken(request);
        }
    }
    
    
    @Service
    public class TokenServiceImpl implements TokenService {
    
        private static final String TOKEN_NAME = "token";
    
        @Autowired
        private RdeisTemplate rdeisTemplate;
    
        @Override
        public Result createToken() {
    
            ...创造Token并放入Redis...
    
            return new Result(...);
        }
    
        @Override
        public void checkToken(HttpServletRequest request) {
            String token = request.getHeader(TOKEN_NAME);
            
            if (StringUtils.isBlank(token)) {//header中不存在token
                token = request.getParameter(TOKEN_NAME);
                if (StringUtils.isBlank(token)) {//parameter中也不存在token
                    throw new Exception(...);
                }
            }
    
            if (...判断Redis中是否有Token...) {
                throw new Exception(...);
            }
    
            ...删除Token并验证是否删除成功(防止多线程问题)...
        }
    }
    
    
    /** 需要先获取Token */
    @RestController
    public class TestController {
    
        @ApiIdempotent
        @PostMapping("/")
        public Result testIdempotence() {
            return new Result();
        }
    }
    
  • 相关阅读:
    (55)ElasticSearch之使用scroll滚动技术实现大数据量搜锁
    (54)ElasticSearch之DocValues解析
    (53)ElasticSearch之如何计算相关度分数
    HDU
    POJ3311 Hie with the Pie
    luoguP2768: 珍珠项链(矩阵乘法优化DP)
    luoguU60884 【模板】动态点分治套线段树
    最小圆覆盖(洛谷 P1742 增量法)
    CodeForces
    HDU
  • 原文地址:https://www.cnblogs.com/loveer/p/12580424.html
Copyright © 2020-2023  润新知